{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "resizable-reorderable-columns-table",
  "title": "Resizable / Reorderable Columns",
  "description": "Drag a header's grip to reorder columns, drag its edge to resize, with the resulting layout persisted to localStorage.",
  "dependencies": [
    "@tanstack/react-table",
    "@dnd-kit/core",
    "@dnd-kit/sortable",
    "@dnd-kit/utilities",
    "@dnd-kit/modifiers"
  ],
  "registryDependencies": [
    "table",
    "button"
  ],
  "files": [
    {
      "path": "src/components/resizable-reorder/columns.tsx",
      "content": "'use client'\n\nimport type { ColumnDef } from '@tanstack/react-table'\n\nexport type User = {\n  id: string\n  name: string\n  email: string\n  role: string\n  status: string\n}\n\nexport const columns: ColumnDef<User>[] = [\n  {\n    id: 'name',\n    accessorKey: 'name',\n    header: 'Name',\n    size: 180,\n    minSize: 100,\n    maxSize: 400,\n  },\n  {\n    id: 'email',\n    accessorKey: 'email',\n    header: 'Email',\n    size: 220,\n    minSize: 140,\n    maxSize: 400,\n  },\n  {\n    id: 'role',\n    accessorKey: 'role',\n    header: 'Role',\n    size: 140,\n    minSize: 80,\n    maxSize: 300,\n  },\n  {\n    id: 'status',\n    accessorKey: 'status',\n    header: 'Status',\n    size: 120,\n    minSize: 80,\n    maxSize: 300,\n  },\n]\n",
      "type": "registry:component",
      "target": "@components/tables/resizable-reorderable-columns-table/columns.tsx"
    },
    {
      "path": "src/components/resizable-reorder/data-table.tsx",
      "content": "'use client'\n\nimport { useEffect, useRef, useState } from 'react'\nimport {\n  DndContext,\n  KeyboardSensor,\n  PointerSensor,\n  closestCenter,\n  useSensor,\n  useSensors,\n} from '@dnd-kit/core'\nimport type { DragEndEvent } from '@dnd-kit/core'\nimport { restrictToHorizontalAxis } from '@dnd-kit/modifiers'\nimport {\n  SortableContext,\n  arrayMove,\n  horizontalListSortingStrategy,\n  sortableKeyboardCoordinates,\n  useSortable,\n} from '@dnd-kit/sortable'\nimport { CSS } from '@dnd-kit/utilities'\nimport {\n  flexRender,\n  getCoreRowModel,\n  useReactTable,\n} from '@tanstack/react-table'\nimport type { ColumnDef, Header } from '@tanstack/react-table'\nimport { GripVertical } from 'lucide-react'\n\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from '#/components/ui/table'\nimport { Button } from '#/components/ui/button'\nimport { cn } from '#/lib/utils.ts'\n\nconst STORAGE_KEY = 'resizable-table-layout'\n\ninterface StoredLayout {\n  columnOrder?: string[]\n  columnSizing?: Record<string, number>\n}\n\nfunction loadLayout(): StoredLayout {\n  try {\n    const raw = window.localStorage.getItem(STORAGE_KEY)\n    return raw ? JSON.parse(raw) : {}\n  } catch {\n    return {}\n  }\n}\n\ninterface ResizableTableProps<TData> {\n  columns: ColumnDef<TData, any>[]\n  data: TData[]\n}\n\nfunction DraggableResizableHeader<TData>({\n  header,\n}: {\n  header: Header<TData, unknown>\n}) {\n  const { attributes, listeners, setNodeRef, transform, transition, isDragging } =\n    useSortable({ id: header.column.id })\n\n  return (\n    <TableHead\n      ref={setNodeRef}\n      style={{\n        width: header.getSize(),\n        transform: CSS.Translate.toString(transform),\n        transition,\n        opacity: isDragging ? 0.6 : 1,\n        position: 'relative',\n      }}\n      className=\"select-none\"\n    >\n      <div className=\"flex items-center gap-1.5 pr-2\">\n        <button\n          type=\"button\"\n          {...attributes}\n          {...listeners}\n          className=\"cursor-grab text-muted-foreground active:cursor-grabbing\"\n          aria-label=\"Reorder column\"\n        >\n          <GripVertical className=\"h-3.5 w-3.5\" />\n        </button>\n        <span>\n          {flexRender(header.column.columnDef.header, header.getContext())}\n        </span>\n      </div>\n      <div\n        onMouseDown={header.getResizeHandler()}\n        onTouchStart={header.getResizeHandler()}\n        className={cn(\n          'absolute top-0 right-0 h-full w-1 cursor-col-resize touch-none bg-border opacity-0 select-none hover:opacity-100',\n          header.column.getIsResizing() && 'bg-primary opacity-100',\n        )}\n      />\n    </TableHead>\n  )\n}\n\nexport function ResizableTable<TData>({ columns, data }: ResizableTableProps<TData>) {\n  const [columnOrder, setColumnOrder] = useState<string[]>(() =>\n    columns.map((c) => c.id as string),\n  )\n  const [columnSizing, setColumnSizing] = useState<Record<string, number>>({})\n  const isHydrated = useRef(false)\n\n  useEffect(() => {\n    const saved = loadLayout()\n    if (saved.columnOrder) setColumnOrder(saved.columnOrder)\n    if (saved.columnSizing) setColumnSizing(saved.columnSizing)\n    isHydrated.current = true\n  }, [])\n\n  useEffect(() => {\n    if (!isHydrated.current) return\n    window.localStorage.setItem(\n      STORAGE_KEY,\n      JSON.stringify({ columnOrder, columnSizing }),\n    )\n  }, [columnOrder, columnSizing])\n\n  const table = useReactTable({\n    data,\n    columns,\n    state: { columnOrder, columnSizing },\n    onColumnOrderChange: setColumnOrder,\n    onColumnSizingChange: setColumnSizing,\n    columnResizeMode: 'onChange',\n    enableColumnResizing: true,\n    getCoreRowModel: getCoreRowModel(),\n  })\n\n  const sensors = useSensors(\n    useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),\n    useSensor(KeyboardSensor, {\n      coordinateGetter: sortableKeyboardCoordinates,\n    }),\n  )\n\n  function handleDragEnd(event: DragEndEvent) {\n    const { active, over } = event\n    if (!over || active.id === over.id) return\n    setColumnOrder((prev) => {\n      const oldIndex = prev.indexOf(active.id as string)\n      const newIndex = prev.indexOf(over.id as string)\n      return arrayMove(prev, oldIndex, newIndex)\n    })\n  }\n\n  function resetLayout() {\n    window.localStorage.removeItem(STORAGE_KEY)\n    setColumnOrder(columns.map((c) => c.id as string))\n    setColumnSizing({})\n  }\n\n  return (\n    <div>\n      <div className=\"mb-3 flex flex-wrap items-center justify-between gap-2\">\n        <p className=\"text-sm text-muted-foreground\">\n          Drag a header's grip to reorder, drag its right edge to resize.\n        </p>\n        <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={resetLayout}>\n          Reset layout\n        </Button>\n      </div>\n      <div className=\"overflow-x-auto rounded-md border\">\n        <DndContext\n          sensors={sensors}\n          collisionDetection={closestCenter}\n          modifiers={[restrictToHorizontalAxis]}\n          onDragEnd={handleDragEnd}\n        >\n          <Table style={{ width: table.getTotalSize() }}>\n            <TableHeader>\n              {table.getHeaderGroups().map((headerGroup) => (\n                <TableRow key={headerGroup.id}>\n                  <SortableContext\n                    items={columnOrder}\n                    strategy={horizontalListSortingStrategy}\n                  >\n                    {headerGroup.headers.map((header) => (\n                      <DraggableResizableHeader 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                  {row.getVisibleCells().map((cell) => (\n                    <TableCell\n                      key={cell.id}\n                      style={{ width: cell.column.getSize() }}\n                    >\n                      {flexRender(\n                        cell.column.columnDef.cell,\n                        cell.getContext(),\n                      )}\n                    </TableCell>\n                  ))}\n                </TableRow>\n              ))}\n            </TableBody>\n          </Table>\n        </DndContext>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/tables/resizable-reorderable-columns-table/data-table.tsx"
    },
    {
      "path": "src/components/resizable-reorder/index.tsx",
      "content": "import { columns } from './columns'\nimport type { User } from './columns'\nimport { ResizableTable } from './data-table'\n\nconst data: User[] = [\n  { id: '1', name: 'Ava Thompson', email: 'ava.t@example.com', role: 'Admin', status: 'Active' },\n  { id: '2', name: 'Liam Chen', email: 'liam.chen@example.com', role: 'Editor', status: 'Active' },\n  { id: '3', name: 'Sofia Patel', email: 'sofia.p@example.com', role: 'Viewer', status: 'Inactive' },\n  { id: '4', name: 'Noah Garcia', email: 'noah.g@example.com', role: 'Editor', status: 'Active' },\n  { id: '5', name: 'Mia Johnson', email: 'mia.j@example.com', role: 'Admin', status: 'Pending' },\n]\n\nexport function ResizableTableDemo() {\n  return <ResizableTable columns={columns} data={data} />\n}\n",
      "type": "registry:component",
      "target": "@components/tables/resizable-reorderable-columns-table/index.tsx"
    }
  ],
  "type": "registry:block"
}