{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "title": "Data Table",
  "description": "Standard and infinite TanStack tables with sorting and visibility state.",
  "dependencies": [
    "@tanstack/react-table@^8.21.3",
    "lucide-react"
  ],
  "registryDependencies": [
    "table",
    "skeleton",
    "button"
  ],
  "files": [
    {
      "path": "components/data-table.tsx",
      "content": "import { flexRender, type Table as ReactTable } from '@tanstack/react-table'\nimport { Skeleton } from '@/components/ui/skeleton'\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from '@/components/ui/table'\n\ninterface DataTableProps<TData> {\n  table: ReactTable<TData>\n  columnCount: number\n  isLoading: boolean\n  emptyLabel: string\n  skeletonRows?: number\n}\n\nexport function DataTable<TData>({\n  table,\n  columnCount,\n  isLoading,\n  emptyLabel,\n  skeletonRows = 3,\n}: DataTableProps<TData>) {\n  const rows = table.getRowModel().rows\n\n  return (\n    <div className=\"overflow-hidden rounded-lg border\">\n      <Table>\n        <TableHeader>\n          {table.getHeaderGroups().map((headerGroup) => (\n            <TableRow key={headerGroup.id}>\n              {headerGroup.headers.map((header) => (\n                <TableHead key={header.id}>\n                  {header.isPlaceholder\n                    ? null\n                    : flexRender(header.column.columnDef.header, header.getContext())}\n                </TableHead>\n              ))}\n            </TableRow>\n          ))}\n        </TableHeader>\n        <TableBody>\n          {isLoading ? (\n            Array.from({ length: skeletonRows }, (_, rowIndex) => (\n              <TableRow key={rowIndex}>\n                {Array.from({ length: columnCount }, (_, cellIndex) => (\n                  <TableCell key={cellIndex}>\n                    <Skeleton className=\"h-6 w-full\" />\n                  </TableCell>\n                ))}\n              </TableRow>\n            ))\n          ) : rows.length === 0 ? (\n            <TableRow>\n              <TableCell className=\"h-24 text-center text-muted-foreground\" colSpan={columnCount}>\n                {emptyLabel}\n              </TableCell>\n            </TableRow>\n          ) : (\n            rows.map((row) => (\n              <TableRow className=\"group\" key={row.id}>\n                {row.getVisibleCells().map((cell) => (\n                  <TableCell key={cell.id}>\n                    {flexRender(cell.column.columnDef.cell, cell.getContext())}\n                  </TableCell>\n                ))}\n              </TableRow>\n            ))\n          )}\n        </TableBody>\n      </Table>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "components/infinite-data-table.tsx",
      "content": "import { flexRender, type Table as ReactTable } from '@tanstack/react-table'\nimport { Skeleton } from '@/components/ui/skeleton'\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from '@/components/ui/table'\nimport { cn } from '@/lib/utils'\n\ninterface InfiniteDataTableProps<TData> {\n  table: ReactTable<TData>\n  columnCount: number\n  isLoading: boolean\n  isRefetching: boolean\n  isFetchingNextPage: boolean\n  hasFilters: boolean\n  emptyLabel: string\n  emptyFilteredLabel: string\n  sentinelRef: (node: HTMLDivElement | null) => void\n}\n\nexport function InfiniteDataTable<TData>({\n  table,\n  columnCount,\n  isLoading,\n  isRefetching,\n  isFetchingNextPage,\n  hasFilters,\n  emptyLabel,\n  emptyFilteredLabel,\n  sentinelRef,\n}: InfiniteDataTableProps<TData>) {\n  const dataRows = table.getRowModel().rows\n\n  return (\n    <div\n      className={cn(\n        'overflow-hidden rounded-lg border transition-opacity',\n        isRefetching && 'opacity-60',\n      )}\n    >\n      <Table>\n        <TableHeader>\n          {table.getHeaderGroups().map((headerGroup) => (\n            <TableRow key={headerGroup.id}>\n              {headerGroup.headers.map((header) => (\n                <TableHead key={header.id}>\n                  {header.isPlaceholder\n                    ? null\n                    : flexRender(header.column.columnDef.header, header.getContext())}\n                </TableHead>\n              ))}\n            </TableRow>\n          ))}\n        </TableHeader>\n        <TableBody>\n          {isLoading ? <SkeletonRows columns={columnCount} /> : null}\n\n          {!isLoading && dataRows.length === 0 ? (\n            <TableRow>\n              <TableCell className=\"h-24 text-center text-muted-foreground\" colSpan={columnCount}>\n                {hasFilters ? emptyFilteredLabel : emptyLabel}\n              </TableCell>\n            </TableRow>\n          ) : null}\n\n          {dataRows.map((row) => (\n            <TableRow className=\"group\" key={row.id}>\n              {row.getVisibleCells().map((cell) => (\n                <TableCell key={cell.id}>\n                  {flexRender(cell.column.columnDef.cell, cell.getContext())}\n                </TableCell>\n              ))}\n            </TableRow>\n          ))}\n\n          {isFetchingNextPage ? <SkeletonRows columns={columnCount} /> : null}\n        </TableBody>\n      </Table>\n      <div ref={sentinelRef} />\n    </div>\n  )\n}\n\nfunction SkeletonRows({ columns }: { columns: number }) {\n  return (\n    <>\n      {Array.from({ length: 5 }, (_, rowIndex) => (\n        <TableRow key={rowIndex}>\n          {Array.from({ length: columns }, (_, cellIndex) => (\n            <TableCell key={cellIndex}>\n              <Skeleton className=\"h-6 w-full\" />\n            </TableCell>\n          ))}\n        </TableRow>\n      ))}\n    </>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "components/sortable-header.tsx",
      "content": "import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react'\nimport { Button } from '@/components/ui/button'\n\nexport interface SortState<Field extends string> {\n  field: Field\n  direction: 'asc' | 'desc'\n}\n\ninterface SortableHeaderProps<Field extends string> {\n  label: string\n  field: Field\n  sort: SortState<Field>\n  onSort: (field: Field) => void\n}\n\nexport function SortableHeader<Field extends string>({\n  label,\n  field,\n  sort,\n  onSort,\n}: SortableHeaderProps<Field>) {\n  const active = sort.field === field\n  const Icon = active ? (sort.direction === 'asc' ? ArrowUp : ArrowDown) : ArrowUpDown\n\n  return (\n    <Button\n      className=\"-ml-2 h-8 cursor-pointer text-muted-foreground data-[active=true]:text-foreground\"\n      data-active={active}\n      onClick={() => onSort(field)}\n      size=\"sm\"\n      variant=\"ghost\"\n    >\n      {label}\n      <Icon className=\"size-3.5\" />\n    </Button>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "hooks/use-data-table.tsx",
      "content": "'use client'\n\nimport {\n  type ColumnDef,\n  type ColumnFiltersState,\n  getCoreRowModel,\n  getFilteredRowModel,\n  getSortedRowModel,\n  type SortingState,\n  type TableOptions,\n  useReactTable,\n  type VisibilityState,\n} from '@tanstack/react-table'\nimport { useEffect, useState } from 'react'\n\ninterface UseDataTableProps<TData> {\n  data: TData[]\n  // biome-ignore lint/suspicious/noExplicitAny: tanstack ColumnDef value type varies per column\n  columns: ColumnDef<TData, any>[]\n  storage?: { key: string; defaultVisibility?: VisibilityState }\n  options?: Omit<Partial<TableOptions<TData>>, 'data' | 'columns'>\n}\n\nconst storageKey = (key: string) => `data-table-visibility-${key}`\n\nfunction readVisibility(key: string): VisibilityState | null {\n  try {\n    const raw = localStorage.getItem(storageKey(key))\n    if (!raw) return null\n    const parsed: unknown = JSON.parse(raw)\n    return parsed && typeof parsed === 'object' ? (parsed as VisibilityState) : null\n  } catch {\n    return null\n  }\n}\n\nexport function useDataTable<TData>({ data, columns, storage, options }: UseDataTableProps<TData>) {\n  const [sorting, setSorting] = useState<SortingState>([])\n  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])\n  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(\n    storage?.defaultVisibility ?? {},\n  )\n\n  const [isClient, setIsClient] = useState(false)\n  useEffect(() => {\n    setIsClient(true)\n  }, [])\n\n  useEffect(() => {\n    if (!isClient || !storage?.key) return\n    const stored = readVisibility(storage.key)\n    if (stored) setColumnVisibility((prev) => ({ ...prev, ...stored }))\n  }, [isClient, storage?.key])\n\n  useEffect(() => {\n    if (!isClient || !storage?.key) return\n    try {\n      localStorage.setItem(storageKey(storage.key), JSON.stringify(columnVisibility))\n    } catch {}\n  }, [columnVisibility, storage?.key, isClient])\n\n  const table = useReactTable({\n    ...options,\n    data,\n    columns,\n    state: { ...options?.state, sorting, columnFilters, columnVisibility },\n    getCoreRowModel: options?.getCoreRowModel ?? getCoreRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    getFilteredRowModel: getFilteredRowModel(),\n    onSortingChange: setSorting,\n    onColumnFiltersChange: setColumnFilters,\n    onColumnVisibilityChange: setColumnVisibility,\n  })\n\n  return {\n    table,\n    sorting,\n    setSorting,\n    columnFilters,\n    setColumnFilters,\n    columnVisibility,\n    setColumnVisibility,\n  }\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:component"
}
