{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sortable-data-table",
  "title": "Sortable data table",
  "description": "An optional controlled data table with mouse, touch and keyboard row reordering.",
  "dependencies": [
    "@tanstack/react-table@^9.2.4",
    "@dnd-kit/core@^6.3.1",
    "@dnd-kit/sortable@^10.0.0",
    "@dnd-kit/modifiers@^9.0.0",
    "@dnd-kit/utilities@^3.2.2",
    "lucide-react@^1.46.0"
  ],
  "registryDependencies": [
    "Familis-co/registry/data-table",
    "Familis-co/registry/button",
    "Familis-co/registry/use-data-table"
  ],
  "files": [
    {
      "path": "registry/familis/blocks/sortable-data-table/sortable-data-table.tsx",
      "content": "\"use client\"\n\nimport { useDataTable } from \"@/registry/familis/ui/hooks/use-data-table\"\n\nimport { useId } from \"react\"\nimport type { Row, RowData, ReactTable } from \"@tanstack/react-table\"\nimport {\n  DndContext,\n  closestCenter,\n  KeyboardSensor,\n  MouseSensor,\n  TouchSensor,\n  useSensor,\n  useSensors,\n  type DragEndEvent,\n} from \"@dnd-kit/core\"\nimport {\n  SortableContext,\n  arrayMove,\n  useSortable,\n  sortableKeyboardCoordinates,\n  verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\"\nimport { restrictToVerticalAxis } from \"@dnd-kit/modifiers\"\nimport { CSS } from \"@dnd-kit/utilities\"\nimport { GripVerticalIcon } from \"lucide-react\"\nimport { DataTableView, type DataTableProps } from \"@/registry/familis/blocks/data-table/data-table\"\nimport type { DataTableFeatures } from \"@/registry/familis/ui/hooks/use-data-table\"\nimport { DataTableRow } from \"@/registry/familis/blocks/data-table/data-table-content\"\nimport { Button } from \"@/registry/familis/ui/button\"\n\nexport type SortableDataTableProps<TData extends RowData> = Omit<\n  DataTableProps<TData>,\n  \"getRowId\" | \"onSelectionChange\" | \"renderRow\" | \"wrapRows\" | \"leadingHeader\"\n> & {\n  getRowId: (row: TData) => string\n  /** The parent owns the reordered data and can persist it. */\n  onDataChange: (data: TData[]) => void\n}\n\nexport function SortableDataTableRow<TData extends RowData>({\n  row,\n  table,\n  disabled,\n  enableSelection,\n}: {\n  row: Row<DataTableFeatures, TData>\n  table: ReactTable<DataTableFeatures, TData>\n  disabled: boolean\n  enableSelection?: boolean\n}) {\n  const {\n    attributes,\n    listeners,\n    setNodeRef,\n    setActivatorNodeRef,\n    transform,\n    transition,\n    isDragging,\n  } = useSortable({ id: row.id, disabled })\n  return (\n    <DataTableRow\n      row={row}\n      table={table}\n      enableSelection={enableSelection}\n      ref={setNodeRef}\n      data-dragging={isDragging}\n      style={{ transform: CSS.Transform.toString(transform), transition }}\n      leadingCell={\n        <Button\n          {...attributes}\n          {...listeners}\n          ref={setActivatorNodeRef}\n          variant=\"ghost\"\n          size=\"icon-sm\"\n          disabled={disabled}\n          aria-label={`Reorder row ${row.id}`}\n        >\n          <GripVerticalIcon aria-hidden=\"true\" />\n        </Button>\n      }\n    />\n  )\n}\n\n/** Optional drag-and-drop composition. Reordering applies to the visible page in source order. */\nexport function SortableDataTable<TData extends RowData>({\n  data,\n  columns,\n  onDataChange,\n  getRowId,\n  pageSize = 10,\n  enableSelection = false,\n  loading = false,\n  ...viewProps\n}: SortableDataTableProps<TData>) {\n  const id = useId()\n  const table = useDataTable({\n    data,\n    columns,\n    getRowId,\n    enableRowSelection: enableSelection,\n    initialState: { pagination: { pageIndex: 0, pageSize: Math.max(1, pageSize) } },\n  })\n  const sensors = useSensors(\n    useSensor(MouseSensor, { activationConstraint: { distance: 8 } }),\n    useSensor(TouchSensor, { activationConstraint: { delay: 250, tolerance: 5 } }),\n    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n  )\n  const disabled = loading || table.state.sorting.length > 0 || table.state.columnFilters.length > 0\n  function onDragEnd({ active, over }: DragEndEvent) {\n    if (disabled || !over || active.id === over.id) return\n    const from = data.findIndex((row) => getRowId(row) === active.id)\n    const to = data.findIndex((row) => getRowId(row) === over.id)\n    if (from < 0 || to < 0) return\n    onDataChange(arrayMove(data, from, to))\n  }\n  return (\n    <DndContext\n      id={id}\n      sensors={sensors}\n      collisionDetection={closestCenter}\n      modifiers={[restrictToVerticalAxis]}\n      onDragEnd={onDragEnd}\n    >\n      <div className=\"flex flex-col gap-2\">\n        {disabled && !loading ? (\n          <p role=\"status\" className=\"text-sm text-muted-foreground\">\n            Clear sorting and filters to reorder rows.\n          </p>\n        ) : null}\n        <DataTableView\n          {...viewProps}\n          table={table}\n          loading={loading}\n          enableSelection={enableSelection}\n          leadingHeader={<span className=\"sr-only\">Reorder</span>}\n          renderRow={(row) => (\n            <SortableDataTableRow\n              key={row.id}\n              row={row}\n              table={table}\n              disabled={disabled}\n              enableSelection={enableSelection}\n            />\n          )}\n          wrapRows={(rows) => (\n            <SortableContext\n              items={table.getRowModel().rows.map((row) => row.id)}\n              strategy={verticalListSortingStrategy}\n            >\n              {rows}\n            </SortableContext>\n          )}\n        />\n      </div>\n    </DndContext>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "base": "base",
    "storybookId": "ui-blocks-sortable-data-table--default"
  },
  "type": "registry:block"
}