{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "type": "registry:ui",
  "title": "Data table",
  "description": "TanStack Table v9 table with sorting, toolbar slot, column visibility and pagination.",
  "dependencies": [
    "@tanstack/react-table",
    "lucide-react"
  ],
  "registryDependencies": [
    "@wibo/utils",
    "@wibo/button",
    "@wibo/dropdown-menu",
    "@wibo/pagination",
    "@wibo/skeleton",
    "@wibo/table"
  ],
  "files": [
    {
      "path": "src/components/data-table.tsx",
      "type": "registry:ui",
      "content": "import { useState, type KeyboardEvent, type ReactNode } from \"react\";\nimport {\n  columnFilteringFeature,\n  columnVisibilityFeature,\n  createColumnHelper,\n  createFilteredRowModel,\n  createPaginatedRowModel,\n  createSortedRowModel,\n  filterFn_includesString,\n  functionalUpdate,\n  globalFilteringFeature,\n  metaHelper,\n  rowPaginationFeature,\n  rowSortingFeature,\n  sortFn_alphanumeric,\n  sortFn_basic,\n  sortFn_datetime,\n  sortFn_text,\n  tableFeatures,\n  useTable,\n  type CellData,\n  type Column,\n  type ColumnDef,\n  type PaginationState,\n  type ReactTable,\n  type RowData,\n  type SortingState,\n  type Updater,\n  type ColumnVisibilityState,\n} from \"@tanstack/react-table\";\nimport { ArrowDown, ArrowUp, ChevronsUpDown, Columns3 } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { PaginationControls, type PaginationControlsLabels } from \"@/components/ui/pagination\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n  Table,\n  TableBody,\n  TableCaption,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\n\n/** Per-column options read by `DataTable` (set through `meta` on a column definition). */\ninterface DataTableColumnMeta {\n  /** Name shown in the column toggle when the header is not a plain string. */\n  label?: string;\n  /** Right-aligned, tabular, monospace cells. */\n  numeric?: boolean;\n  headerClassName?: string;\n  cellClassName?: string;\n}\n\n/** The TanStack Table v9 features every `DataTable` registers. */\nconst dataTableFeatures = tableFeatures({\n  rowSortingFeature,\n  columnVisibilityFeature,\n  columnFilteringFeature,\n  globalFilteringFeature,\n  rowPaginationFeature,\n  filteredRowModel: createFilteredRowModel(),\n  sortedRowModel: createSortedRowModel(),\n  paginatedRowModel: createPaginatedRowModel(),\n  sortFns: {\n    alphanumeric: sortFn_alphanumeric,\n    text: sortFn_text,\n    datetime: sortFn_datetime,\n    basic: sortFn_basic,\n  },\n  filterFns: { includesString: filterFn_includesString },\n  columnMeta: metaHelper<DataTableColumnMeta>(),\n});\n\ntype DataTableFeatures = typeof dataTableFeatures;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- columns hold heterogeneous value types\ntype DataTableColumnDef<TData extends RowData> = ColumnDef<DataTableFeatures, TData, any>;\ntype DataTableInstance<TData extends RowData> = ReactTable<DataTableFeatures, TData>;\n\n/** Typed column helper for `DataTable` columns. Call it at module scope. */\nfunction createDataTableColumnHelper<TData extends RowData>() {\n  return createColumnHelper<DataTableFeatures, TData>();\n}\n\nfunction useSlice<T>(\n  value: T | undefined,\n  onChange: ((value: T) => void) | undefined,\n  initial: T,\n): [T, (updater: Updater<T>) => void] {\n  const [internal, setInternal] = useState<T>(initial);\n  const controlled = value !== undefined;\n  const current = controlled ? value : internal;\n  const update = (updater: Updater<T>) => {\n    const next = functionalUpdate(updater, current);\n    if (!controlled) setInternal(next);\n    onChange?.(next);\n  };\n  return [current, update];\n}\n\ninterface DataTableProps<TData extends RowData> {\n  /** Keep columns stable: define them at module scope or memoise them. */\n  columns: DataTableColumnDef<TData>[];\n  data: TData[];\n  getRowId?: (row: TData, index: number) => string;\n  /** Slot above the table, usually a `TableToolbar`. A function receives the table instance. */\n  toolbar?: ReactNode | ((table: DataTableInstance<TData>) => ReactNode);\n  /** Text search across string and number columns (client side). */\n  globalFilter?: string;\n  sorting?: SortingState;\n  defaultSorting?: SortingState;\n  onSortingChange?: (sorting: SortingState) => void;\n  /** The server sorts: `data` arrives already ordered. */\n  manualSorting?: boolean;\n  columnVisibility?: ColumnVisibilityState;\n  defaultColumnVisibility?: ColumnVisibilityState;\n  onColumnVisibilityChange?: (visibility: ColumnVisibilityState) => void;\n  /** Enables client pagination with this many rows per page. */\n  pageSize?: number;\n  pageSizeOptions?: number[];\n  paginationLabels?: Partial<PaginationControlsLabels>;\n  onRowClick?: (row: TData) => void;\n  loading?: boolean;\n  /** Skeleton rows while loading. */\n  loadingRows?: number;\n  /** Shown in the table's one cell when there are no rows. Pass an `EmptyState variant=\"plain\"` for first-use screens: the table is its frame. */\n  emptyState?: ReactNode;\n  /** Accessible table caption (visually hidden). */\n  caption?: ReactNode;\n  className?: string;\n}\n\n/** Sortable data table on TanStack Table v9 with a toolbar slot, column visibility and optional pagination. */\nfunction DataTable<TData extends RowData>({\n  columns,\n  data,\n  getRowId,\n  toolbar,\n  globalFilter: globalFilterProp,\n  sorting: sortingProp,\n  defaultSorting = [],\n  onSortingChange,\n  manualSorting = false,\n  columnVisibility: visibilityProp,\n  defaultColumnVisibility = {},\n  onColumnVisibilityChange,\n  pageSize,\n  pageSizeOptions,\n  paginationLabels,\n  onRowClick,\n  loading = false,\n  loadingRows = 5,\n  emptyState = \"No results.\",\n  caption,\n  className,\n}: DataTableProps<TData>) {\n  const [sorting, setSorting] = useSlice(sortingProp, onSortingChange, defaultSorting);\n  const [columnVisibility, setColumnVisibility] = useSlice(\n    visibilityProp,\n    onColumnVisibilityChange,\n    defaultColumnVisibility,\n  );\n  const [globalFilter, setGlobalFilter] = useSlice<string>(globalFilterProp, undefined, \"\");\n  const [pagination, setPagination] = useSlice<PaginationState>(undefined, undefined, {\n    pageIndex: 0,\n    pageSize: pageSize ?? 10,\n  });\n\n  const table = useTable({\n    features: dataTableFeatures,\n    columns,\n    data,\n    getRowId,\n    state: { sorting, columnVisibility, globalFilter, pagination },\n    onSortingChange: setSorting,\n    onColumnVisibilityChange: setColumnVisibility,\n    onGlobalFilterChange: setGlobalFilter,\n    onPaginationChange: setPagination,\n    manualSorting,\n    globalFilterFn: \"includesString\",\n  });\n\n  const paginated = pageSize !== undefined;\n  const rows = paginated ? table.getRowModel().rows : table.getPrePaginatedRowModel().rows;\n  const visibleColumns = table.getVisibleLeafColumns();\n\n  const rowKeyDown = (event: KeyboardEvent<HTMLTableRowElement>, original: TData) => {\n    if (event.target !== event.currentTarget) return;\n    if (event.key === \"Enter\" || event.key === \" \") {\n      event.preventDefault();\n      onRowClick?.(original);\n    }\n  };\n\n  return (\n    <div data-slot=\"data-table\" className={cn(\"flex flex-col gap-3\", className)}>\n      {typeof toolbar === \"function\" ? toolbar(table) : toolbar}\n      <div\n        className=\"overflow-hidden rounded-xl border border-border bg-card shadow-card\"\n        aria-busy={loading || undefined}\n      >\n        <Table>\n          {caption ? <TableCaption className=\"sr-only\">{caption}</TableCaption> : null}\n          <TableHeader>\n            {table.getHeaderGroups().map((group) => (\n              <TableRow key={group.id} className=\"hover:bg-transparent\">\n                {group.headers.map((header) => {\n                  const column = header.column;\n                  const meta = column.columnDef.meta;\n                  const sorted = column.getIsSorted();\n                  const canSort = column.getCanSort();\n                  const headerDef = column.columnDef.header;\n                  return (\n                    <TableHead\n                      key={header.id}\n                      colSpan={header.colSpan}\n                      numeric={meta?.numeric}\n                      className={meta?.headerClassName}\n                      aria-sort={\n                        sorted === \"asc\"\n                          ? \"ascending\"\n                          : sorted === \"desc\"\n                            ? \"descending\"\n                            : canSort\n                              ? \"none\"\n                              : undefined\n                      }\n                    >\n                      {header.isPlaceholder ? null : typeof headerDef === \"string\" && canSort ? (\n                        <DataTableColumnHeader column={column} title={headerDef} />\n                      ) : (\n                        <table.FlexRender header={header} />\n                      )}\n                    </TableHead>\n                  );\n                })}\n              </TableRow>\n            ))}\n          </TableHeader>\n          <TableBody>\n            {loading ? (\n              Array.from({ length: loadingRows }, (_, index) => (\n                <TableRow key={`loading-${index}`} className=\"hover:bg-transparent\">\n                  {visibleColumns.map((column) => (\n                    <TableCell key={column.id}>\n                      <Skeleton className=\"h-4 w-full max-w-32\" />\n                    </TableCell>\n                  ))}\n                </TableRow>\n              ))\n            ) : rows.length > 0 ? (\n              rows.map((row) => (\n                <TableRow\n                  key={row.id}\n                  data-clickable={onRowClick ? true : undefined}\n                  tabIndex={onRowClick ? 0 : undefined}\n                  onClick={onRowClick ? () => onRowClick(row.original) : undefined}\n                  onKeyDown={onRowClick ? (event) => rowKeyDown(event, row.original) : undefined}\n                  className={cn(\n                    onRowClick &&\n                      \"cursor-pointer outline-none focus-visible:bg-foreground/4 focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset\",\n                  )}\n                >\n                  {row.getVisibleCells().map((cell) => (\n                    <TableCell\n                      key={cell.id}\n                      numeric={cell.column.columnDef.meta?.numeric}\n                      className={cell.column.columnDef.meta?.cellClassName}\n                    >\n                      <table.FlexRender cell={cell} />\n                    </TableCell>\n                  ))}\n                </TableRow>\n              ))\n            ) : (\n              <TableRow className=\"hover:bg-transparent\">\n                <TableCell\n                  colSpan={Math.max(visibleColumns.length, 1)}\n                  className=\"h-24 text-center whitespace-normal text-muted-foreground\"\n                >\n                  {emptyState}\n                </TableCell>\n              </TableRow>\n            )}\n          </TableBody>\n        </Table>\n      </div>\n      {paginated && !loading ? (\n        <PaginationControls\n          pageIndex={pagination.pageIndex}\n          pageCount={table.getPageCount()}\n          onPageChange={(pageIndex) => table.setPageIndex(pageIndex)}\n          pageSize={pagination.pageSize}\n          totalCount={table.getPrePaginatedRowModel().rows.length}\n          onPageSizeChange={(size) => table.setPageSize(size)}\n          pageSizeOptions={pageSizeOptions}\n          labels={paginationLabels}\n        />\n      ) : null}\n    </div>\n  );\n}\n\ninterface DataTableColumnHeaderProps<TData extends RowData, TValue extends CellData> {\n  column: Column<DataTableFeatures, TData, TValue>;\n  title: ReactNode;\n  className?: string;\n}\n\n/** Sortable header button. `DataTable` uses it automatically for string headers. */\nfunction DataTableColumnHeader<TData extends RowData, TValue extends CellData>({\n  column,\n  title,\n  className,\n}: DataTableColumnHeaderProps<TData, TValue>) {\n  if (!column.getCanSort()) return <span className={className}>{title}</span>;\n  const sorted = column.getIsSorted();\n  const Icon = sorted === \"asc\" ? ArrowUp : sorted === \"desc\" ? ArrowDown : ChevronsUpDown;\n  return (\n    <button\n      type=\"button\"\n      onClick={column.getToggleSortingHandler()}\n      className={cn(\n        \"-mx-1.5 inline-flex h-6 cursor-pointer items-center gap-1 rounded-md px-1.5 text-xs font-medium text-muted-foreground transition-[color] duration-150 outline-none hover:bg-foreground/4 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50\",\n        sorted && \"text-foreground\",\n        className,\n      )}\n    >\n      {title}\n      <Icon aria-hidden=\"true\" className=\"size-3.5\" />\n    </button>\n  );\n}\n\nfunction columnLabel<TData extends RowData>(column: Column<DataTableFeatures, TData, CellData>) {\n  const header = column.columnDef.header;\n  return column.columnDef.meta?.label ?? (typeof header === \"string\" ? header : column.id);\n}\n\ninterface DataTableColumnToggleProps<TData extends RowData> {\n  table: DataTableInstance<TData>;\n  label?: string;\n}\n\n/** Column visibility menu, for the toolbar's column slot. */\nfunction DataTableColumnToggle<TData extends RowData>({\n  table,\n  label = \"Columns\",\n}: DataTableColumnToggleProps<TData>) {\n  const columns = table.getAllLeafColumns().filter((column) => column.getCanHide());\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"outline\" size=\"sm\">\n          <Columns3 aria-hidden=\"true\" />\n          {label}\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" className=\"w-48\">\n        <DropdownMenuLabel>{label}</DropdownMenuLabel>\n        <DropdownMenuSeparator />\n        {columns.map((column) => (\n          <DropdownMenuCheckboxItem\n            key={column.id}\n            checked={column.getIsVisible()}\n            onCheckedChange={(value) => column.toggleVisibility(Boolean(value))}\n            onSelect={(event) => event.preventDefault()}\n          >\n            {columnLabel(column)}\n          </DropdownMenuCheckboxItem>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nexport {\n  DataTable,\n  DataTableColumnHeader,\n  DataTableColumnToggle,\n  createDataTableColumnHelper,\n  dataTableFeatures,\n};\nexport type {\n  DataTableColumnDef,\n  DataTableColumnMeta,\n  DataTableFeatures,\n  DataTableInstance,\n  DataTableProps,\n};\n"
    }
  ]
}
