{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table-16",
  "title": "Reorderable columns",
  "description": "Drag a column by its handle to move it, and sort from its header.",
  "dependencies": [
    "@dnd-kit/core",
    "@dnd-kit/modifiers",
    "@dnd-kit/sortable",
    "@dnd-kit/utilities",
    "@tanstack/react-table",
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/badge.json",
    "https://ui.ndekocode.dev/r/button.json",
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/table.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/table/table-16.tsx",
      "content": "'use client';\n\nimport {\n  closestCenter,\n  DndContext,\n  type DragEndEvent,\n  KeyboardSensor,\n  MouseSensor,\n  TouchSensor,\n  useSensor,\n  useSensors,\n} from '@dnd-kit/core';\nimport { restrictToHorizontalAxis } from '@dnd-kit/modifiers';\nimport {\n  arrayMove,\n  horizontalListSortingStrategy,\n  SortableContext,\n  sortableKeyboardCoordinates,\n  useSortable,\n} from '@dnd-kit/sortable';\nimport { CSS, type Transform } from '@dnd-kit/utilities';\nimport {\n  type Cell,\n  type Column,\n  type ColumnDef,\n  flexRender,\n  getCoreRowModel,\n  getSortedRowModel,\n  type Header,\n  type SortDirection,\n  type SortingState,\n  useReactTable,\n} from '@tanstack/react-table';\nimport {\n  AnimatePresence,\n  motion,\n  type Transition,\n  type Variants,\n} from 'motion/react';\nimport { useId, useState } from 'react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport {\n  ChevronDownIcon,\n  ChevronsUpDownIcon,\n  ChevronUpIcon,\n  GripVerticalIcon,\n} from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Badge } from '@/registry/ndeko/ui/badge';\nimport { Button } from '@/registry/ndeko/ui/button';\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from '@/registry/ndeko/ui/table';\n\ntype Status = 'Active' | 'Inactive' | 'Pending';\n\ntype Account = {\n  id: string;\n  name: string;\n  email: string;\n  location: string;\n  status: Status;\n  balance: number;\n};\n\nconst STATUS_VARIANTS = {\n  Active: 'success',\n  Inactive: 'secondary',\n  Pending: 'warning',\n} as const;\n\nconst ACCOUNTS: Account[] = [\n  {\n    id: '1',\n    name: 'Jordan Hayes',\n    email: 'jordan.hayes@example.com',\n    location: 'Toronto, Canada',\n    status: 'Active',\n    balance: 1250,\n  },\n  {\n    id: '2',\n    name: 'Anna Novak',\n    email: 'anna.novak@example.com',\n    location: 'Prague, Czechia',\n    status: 'Active',\n    balance: 600,\n  },\n  {\n    id: '3',\n    name: 'Kenji Watanabe',\n    email: 'kenji.watanabe@example.com',\n    location: 'Osaka, Japan',\n    status: 'Inactive',\n    balance: 650,\n  },\n  {\n    id: '4',\n    name: 'Lina Haddad',\n    email: 'lina.haddad@example.com',\n    location: 'Lyon, France',\n    status: 'Active',\n    balance: 0,\n  },\n  {\n    id: '5',\n    name: 'Ama Mensah',\n    email: 'ama.mensah@example.com',\n    location: 'Accra, Ghana',\n    status: 'Pending',\n    balance: -1000,\n  },\n];\n\nconst CURRENCY = new Intl.NumberFormat('en-US', {\n  style: 'currency',\n  currency: 'USD',\n});\n\nconst FIGURE_COLUMNS = new Set(['balance']);\n\nconst COLUMNS: ColumnDef<Account>[] = [\n  {\n    id: 'name',\n    header: 'Name',\n    accessorKey: 'name',\n    sortDescFirst: false,\n    cell: ({ row }) => <span className='font-medium'>{row.original.name}</span>,\n  },\n  {\n    id: 'email',\n    header: 'Email',\n    accessorKey: 'email',\n    cell: ({ row }) => (\n      <span className='text-muted-foreground'>{row.original.email}</span>\n    ),\n  },\n  {\n    id: 'location',\n    header: 'Location',\n    accessorKey: 'location',\n  },\n  {\n    id: 'status',\n    header: 'Status',\n    accessorKey: 'status',\n    cell: ({ row }) => (\n      <Badge variant={STATUS_VARIANTS[row.original.status]}>\n        {row.original.status}\n      </Badge>\n    ),\n  },\n  {\n    id: 'balance',\n    header: 'Balance',\n    accessorKey: 'balance',\n    cell: ({ row }) => CURRENCY.format(row.original.balance),\n  },\n];\n\nconst INITIAL_ORDER = COLUMNS.map((column) => String(column.id));\n\nconst TRAVEL = { duration: 200, easing: 'cubic-bezier(0.23, 1, 0.32, 1)' };\n\nconst SWAP_TRANSITION: Transition = { duration: 0.15, ease: [0.2, 0, 0, 1] };\n\nconst GLYPH_VARIANTS: Variants = {\n  enter: { opacity: 0, scale: 0.6, filter: 'blur(2px)' },\n  visible: { opacity: 1, scale: 1, filter: 'blur(0px)' },\n  exit: { opacity: 0, scale: 0.6, filter: 'blur(2px)' },\n};\n\nconst FADE_VARIANTS: Variants = {\n  enter: { opacity: 0 },\n  visible: { opacity: 1 },\n  exit: { opacity: 0 },\n};\n\nfunction isFigureColumn(columnId: string) {\n  return FIGURE_COLUMNS.has(columnId);\n}\n\nfunction alignClassOf(columnId: string) {\n  if (isFigureColumn(columnId)) {\n    return 'text-right tabular-nums';\n  }\n  return undefined;\n}\n\nfunction labelOf(column: Column<Account>) {\n  const header = column.columnDef.header;\n  if (typeof header === 'string') {\n    return header;\n  }\n  return column.id;\n}\n\nfunction ariaSortOf(column: Column<Account>) {\n  const direction = column.getIsSorted();\n  if (direction === 'asc') {\n    return 'ascending';\n  }\n  if (direction === 'desc') {\n    return 'descending';\n  }\n  return 'none';\n}\n\nfunction headLayoutOf(columnId: string) {\n  if (isFigureColumn(columnId)) {\n    return 'justify-between';\n  }\n  return undefined;\n}\n\nfunction sortButtonOrderOf(columnId: string) {\n  if (isFigureColumn(columnId)) {\n    return 'flex-row-reverse';\n  }\n  return undefined;\n}\n\nfunction draggingAttributeOf(isDragging: boolean) {\n  if (isDragging) {\n    return '';\n  }\n  return undefined;\n}\n\nfunction layerOf(isDragging: boolean) {\n  if (isDragging) {\n    return 1;\n  }\n  return 0;\n}\n\nfunction sortableStyleOf(\n  transform: Transform | null,\n  transition: string | undefined,\n  isDragging: boolean,\n  width: number\n): React.CSSProperties {\n  return {\n    position: 'relative',\n    transform: CSS.Translate.toString(transform),\n    transition,\n    width,\n    zIndex: layerOf(isDragging),\n  };\n}\n\nfunction glyphVariantsOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return FADE_VARIANTS;\n  }\n  return GLYPH_VARIANTS;\n}\n\nfunction sortGlyphOf(direction: false | SortDirection) {\n  if (direction === 'asc') {\n    return {\n      key: 'asc',\n      icon: <ChevronUpIcon className='text-foreground size-3.5' />,\n    };\n  }\n  if (direction === 'desc') {\n    return {\n      key: 'desc',\n      icon: <ChevronDownIcon className='text-foreground size-3.5' />,\n    };\n  }\n  return {\n    key: 'none',\n    icon: <ChevronsUpDownIcon className='size-3.5 opacity-40' />,\n  };\n}\n\nfunction SortGlyph({ direction }: { direction: false | SortDirection }) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const variants = glyphVariantsOf(shouldReduceMotion);\n  const glyph = sortGlyphOf(direction);\n\n  return (\n    <span aria-hidden='true' className='relative inline-flex size-3.5'>\n      <AnimatePresence mode='popLayout' initial={false}>\n        <motion.span\n          key={glyph.key}\n          className='inline-flex'\n          variants={variants}\n          initial='enter'\n          animate='visible'\n          exit='exit'\n          transition={SWAP_TRANSITION}\n        >\n          {glyph.icon}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\nfunction DraggableHead({ header }: { header: Header<Account, unknown> }) {\n  const { column } = header;\n  const {\n    attributes,\n    isDragging,\n    listeners,\n    setNodeRef,\n    transform,\n    transition,\n  } = useSortable({ id: column.id, transition: TRAVEL });\n  const style = sortableStyleOf(\n    transform,\n    transition,\n    isDragging,\n    column.getSize()\n  );\n  const label = labelOf(column);\n\n  return (\n    <TableHead\n      ref={setNodeRef}\n      style={style}\n      aria-sort={ariaSortOf(column)}\n      data-dragging={draggingAttributeOf(isDragging)}\n      className='before:bg-border data-dragging:bg-card relative whitespace-nowrap transition-[background-color] before:absolute before:inset-y-2 before:start-0 before:w-(--hairline) first:before:hidden data-dragging:shadow-[0_4px_12px_rgb(0_0_0/0.08)]'\n    >\n      <div className={cn('flex items-center gap-0.5', headLayoutOf(column.id))}>\n        <Button\n          variant='ghost'\n          size='icon-sm'\n          className='text-muted-foreground hover:text-foreground -ms-2 cursor-grab in-data-dragging:cursor-grabbing'\n          {...attributes}\n          {...listeners}\n          aria-label={`Reorder the ${label} column`}\n        >\n          <GripVerticalIcon />\n        </Button>\n        <Button\n          variant='ghost'\n          size='sm'\n          onClick={column.getToggleSortingHandler()}\n          className={cn(\n            'text-muted-foreground hover:text-foreground gap-1.5 px-1.5',\n            sortButtonOrderOf(column.id)\n          )}\n        >\n          {flexRender(column.columnDef.header, header.getContext())}\n          <SortGlyph direction={column.getIsSorted()} />\n        </Button>\n      </div>\n    </TableHead>\n  );\n}\n\nfunction DragAlongCell({ cell }: { cell: Cell<Account, unknown> }) {\n  const { column } = cell;\n  const { isDragging, setNodeRef, transform, transition } = useSortable({\n    id: column.id,\n    transition: TRAVEL,\n  });\n  const style = sortableStyleOf(\n    transform,\n    transition,\n    isDragging,\n    column.getSize()\n  );\n\n  return (\n    <TableCell\n      ref={setNodeRef}\n      style={style}\n      data-dragging={draggingAttributeOf(isDragging)}\n      className={cn('data-dragging:bg-card truncate', alignClassOf(column.id))}\n    >\n      {flexRender(column.columnDef.cell, cell.getContext())}\n    </TableCell>\n  );\n}\n\nexport default function Table16() {\n  const dndId = useId();\n  const [sorting, setSorting] = useState<SortingState>([]);\n  const [columnOrder, setColumnOrder] = useState<string[]>(INITIAL_ORDER);\n  const sensors = useSensors(\n    useSensor(MouseSensor),\n    useSensor(TouchSensor),\n    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })\n  );\n\n  // eslint-disable-next-line react-hooks/incompatible-library -- TanStack Table v8 is required and cannot be memoized by the React Compiler.\n  const table = useReactTable({\n    data: ACCOUNTS,\n    columns: COLUMNS,\n    state: { sorting, columnOrder },\n    onSortingChange: setSorting,\n    onColumnOrderChange: setColumnOrder,\n    getCoreRowModel: getCoreRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    enableSortingRemoval: false,\n  });\n\n  function moveColumn(event: DragEndEvent) {\n    const { active, over } = event;\n    if (!over) {\n      return;\n    }\n    const activeId = String(active.id);\n    const overId = String(over.id);\n    if (activeId === overId) {\n      return;\n    }\n    setColumnOrder((current) => {\n      const fromIndex = current.indexOf(activeId);\n      const toIndex = current.indexOf(overId);\n      return arrayMove(current, fromIndex, toIndex);\n    });\n  }\n\n  return (\n    <div className='w-full'>\n      <DndContext\n        id={dndId}\n        collisionDetection={closestCenter}\n        modifiers={[restrictToHorizontalAxis]}\n        onDragEnd={moveColumn}\n        sensors={sensors}\n      >\n        <Table>\n          <TableHeader>\n            {table.getHeaderGroups().map((headerGroup) => (\n              <TableRow key={headerGroup.id} className='hover:bg-transparent'>\n                <SortableContext\n                  items={columnOrder}\n                  strategy={horizontalListSortingStrategy}\n                >\n                  {headerGroup.headers.map((header) => (\n                    <DraggableHead key={header.id} header={header} />\n                  ))}\n                </SortableContext>\n              </TableRow>\n            ))}\n          </TableHeader>\n          <TableBody>\n            {table.getRowModel().rows.map((row) => (\n              <TableRow key={row.id}>\n                <SortableContext\n                  items={columnOrder}\n                  strategy={horizontalListSortingStrategy}\n                >\n                  {row.getVisibleCells().map((cell) => (\n                    <DragAlongCell key={cell.id} cell={cell} />\n                  ))}\n                </SortableContext>\n              </TableRow>\n            ))}\n          </TableBody>\n        </Table>\n      </DndContext>\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "components",
    "table"
  ],
  "type": "registry:component"
}