{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "title": "data-table",
  "description": "A TanStack Table v9 data table with sorting, selection, pagination, expansion, and opt-in stacked rows with configurable container breakpoints. Responsive states sit outside the scroller.",
  "dependencies": [
    "@tanstack/react-table@^9.3.1"
  ],
  "registryDependencies": [
    "@dwellir/dwellir-theme",
    "@dwellir/utils",
    "@dwellir/table",
    "@dwellir/button",
    "@dwellir/checkbox",
    "@dwellir/dropdown-menu",
    "@dwellir/empty",
    "@dwellir/pagination",
    "@dwellir/skeleton",
    "@dwellir/rune-icons"
  ],
  "files": [
    {
      "path": "components/ui/data-table.tsx",
      "content": "'use client';\n\nimport {\n  type CellData,\n  type Column,\n  type ColumnDef,\n  columnVisibilityFeature,\n  createPaginatedRowModel,\n  createSortedRowModel,\n  flexRender,\n  type Header,\n  metaHelper,\n  type Row,\n  type RowData,\n  rowExpandingFeature,\n  rowPaginationFeature,\n  rowSelectionFeature,\n  rowSortingFeature,\n  sortFn_alphanumeric,\n  sortFn_basic,\n  sortFn_datetime,\n  sortFn_text,\n  type Table as TableInstance,\n  type TableOptions,\n  tableFeatures,\n  useTable,\n} from '@tanstack/react-table';\nimport { type ComponentProps, type ReactNode, useMemo } from 'react';\nimport {\n  RuneArrowDown,\n  RuneArrowLeft,\n  RuneArrowRight,\n  RuneArrowUp,\n  RuneArrowUpDown,\n  RuneColumns,\n} from '@/components/brand/rune-icons';\nimport { Button } from '@/components/ui/button';\nimport { Checkbox } from '@/components/ui/checkbox';\nimport { DataTableRows } from '@/components/ui/data-table-rows';\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuLabel,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport { Pagination, PaginationContent, PaginationItem } from '@/components/ui/pagination';\nimport { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table';\nimport { cn } from '@/lib/utils';\n\ntype DataTableColumnMeta = {\n  /** The identity, actions, or a field omitted only in stacked mode. */\n  stackRole?: 'title' | 'action' | 'hidden';\n  /** Plain label for a stacked value when the header renders a control. */\n  stackLabel?: string;\n};\n\n/*\n * The TanStack Table v9 features DataTable registers. Type columns with\n * DataTableColumnDef so they see these features and the stacked-mode meta.\n * Auto sorting picks one of the four sort functions by the column's values.\n */\nconst dataTableFeatures = tableFeatures({\n  columnVisibilityFeature,\n  rowExpandingFeature,\n  rowPaginationFeature,\n  rowSelectionFeature,\n  rowSortingFeature,\n  paginatedRowModel: createPaginatedRowModel(),\n  sortedRowModel: createSortedRowModel(),\n  sortFns: {\n    alphanumeric: sortFn_alphanumeric,\n    basic: sortFn_basic,\n    datetime: sortFn_datetime,\n    text: sortFn_text,\n  },\n  columnMeta: metaHelper<DataTableColumnMeta>(),\n});\n\ntype DataTableFeatures = typeof dataTableFeatures;\n\ntype DataTableColumnDef<TData extends RowData, TValue extends CellData = CellData> = ColumnDef<\n  DataTableFeatures,\n  TData,\n  TValue\n>;\n\n/*\n * A sortable column header. A header given as a string gets one by default;\n * pass it yourself to change the title. A column with enableSorting: false\n * shows the plain title.\n */\nfunction DataTableColumnHeader<TData extends RowData, TValue extends CellData>({\n  column,\n  title,\n  className,\n}: {\n  column: Column<DataTableFeatures, TData, TValue>;\n  title: ReactNode;\n  className?: string;\n}) {\n  if (!column.getCanSort()) return <span data-slot=\"data-table-column-title\">{title}</span>;\n  const sorted = column.getIsSorted();\n  const Icon = sorted === 'asc' ? RuneArrowUp : sorted === 'desc' ? RuneArrowDown : RuneArrowUpDown;\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"data-table-sort\"\n      data-sorted={sorted || undefined}\n      onClick={column.getToggleSortingHandler()}\n      className={cn(\n        'inline-flex cursor-pointer items-center gap-1.5 rounded-sm uppercase transition-colors select-none group-data-[head=quiet]/table:normal-case outline-none hover:text-foreground focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring data-[sorted]:text-foreground',\n        className,\n      )}\n    >\n      {title}\n      <Icon aria-hidden=\"true\" className={cn('size-3.5', sorted ? '' : 'opacity-60')} />\n    </button>\n  );\n}\n\n/* The checkbox column: selects the rows of the page, or one row. Some rows picked shows the mixed state. */\nfunction selectionColumn<TData extends RowData>(): DataTableColumnDef<TData> {\n  return {\n    id: 'select',\n    meta: { stackRole: 'action', stackLabel: 'Select row' },\n    enableSorting: false,\n    enableHiding: false,\n    header: ({ table }) => (\n      <Checkbox\n        aria-label=\"Select every row on this page\"\n        checked={\n          table.getIsAllPageRowsSelected() ||\n          (table.getIsSomePageRowsSelected() ? 'indeterminate' : false)\n        }\n        onCheckedChange={(value) => table.toggleAllPageRowsSelected(value === true)}\n      />\n    ),\n    cell: ({ row }) => (\n      <Checkbox\n        aria-label=\"Select row\"\n        checked={row.getIsSelected()}\n        disabled={!row.getCanSelect()}\n        onCheckedChange={(value) => row.toggleSelected(value === true)}\n      />\n    ),\n  };\n}\n\nfunction ariaSort<TData extends RowData>(header: Header<DataTableFeatures, TData, CellData>) {\n  if (!header.column.getCanSort()) return undefined;\n  const sorted = header.column.getIsSorted();\n  return sorted === 'asc' ? 'ascending' : sorted === 'desc' ? 'descending' : 'none';\n}\n\nfunction HeaderCell<TData extends RowData>({\n  header,\n  stacked,\n}: {\n  header: Header<DataTableFeatures, TData, CellData>;\n  stacked: boolean;\n}) {\n  const { columnDef } = header.column;\n  let content: ReactNode = null;\n  if (!header.isPlaceholder) {\n    content =\n      typeof columnDef.header === 'string' ? (\n        <DataTableColumnHeader column={header.column} title={columnDef.header} />\n      ) : (\n        flexRender(columnDef.header, header.getContext())\n      );\n  }\n  return (\n    <TableHead\n      colSpan={header.colSpan}\n      aria-sort={ariaSort(header)}\n      stackRole={stacked ? columnDef.meta?.stackRole : undefined}\n    >\n      {content}\n    </TableHead>\n  );\n}\n\n/* The columns menu: a checkbox per column that can hide. */\nfunction DataTableViewOptions<TData extends RowData>({\n  table,\n}: {\n  table: TableInstance<DataTableFeatures, TData>;\n}) {\n  const columns = table.getAllLeafColumns().filter((column) => column.getCanHide());\n  if (!columns.length) return null;\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button data-slot=\"data-table-view-options\" variant=\"outline\" size=\"sm\" shape=\"rounded\">\n          <RuneColumns size={24} aria-hidden=\"true\" />\n          Columns\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" size=\"sm\">\n        <DropdownMenuLabel>Show columns</DropdownMenuLabel>\n        {columns.map((column) => {\n          const { header } = column.columnDef;\n          return (\n            <DropdownMenuCheckboxItem\n              key={column.id}\n              checked={column.getIsVisible()}\n              onCheckedChange={(value) => column.toggleVisibility(value === true)}\n              onSelect={(event) => event.preventDefault()}\n            >\n              {typeof header === 'string' ? header : column.id}\n            </DropdownMenuCheckboxItem>\n          );\n        })}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\n/* Previous and next, the page count and, with selection, the number of rows picked. */\nfunction DataTablePagination<TData extends RowData>({\n  table,\n  selectable = false,\n  label = 'Table pages',\n}: {\n  table: TableInstance<DataTableFeatures, TData>;\n  selectable?: boolean;\n  label?: string;\n}) {\n  const pages = Math.max(table.getPageCount(), 1);\n  const page = Math.min(table.store.state.pagination.pageIndex + 1, pages);\n  return (\n    <div\n      data-slot=\"data-table-pagination\"\n      className=\"flex flex-wrap items-center justify-between gap-3 text-sm text-muted-foreground\"\n    >\n      <p data-slot=\"data-table-status\" aria-live=\"polite\">\n        {selectable\n          ? `${table.getSelectedRowModel().rows.length} of ${table.getCoreRowModel().rows.length} rows selected`\n          : `Page ${page} of ${pages}`}\n      </p>\n      <Pagination aria-label={label} className=\"mx-0 w-auto\">\n        <PaginationContent>\n          {selectable ? (\n            <PaginationItem>\n              <span data-slot=\"data-table-page\" className=\"px-2\">\n                Page {page} of {pages}\n              </span>\n            </PaginationItem>\n          ) : null}\n          <PaginationItem>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              shape=\"rounded\"\n              onClick={() => table.previousPage()}\n              disabled={!table.getCanPreviousPage()}\n            >\n              <RuneArrowLeft size={24} aria-hidden=\"true\" />\n              Previous\n            </Button>\n          </PaginationItem>\n          <PaginationItem>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              shape=\"rounded\"\n              onClick={() => table.nextPage()}\n              disabled={!table.getCanNextPage()}\n            >\n              Next\n              <RuneArrowRight size={24} aria-hidden=\"true\" />\n            </Button>\n          </PaginationItem>\n        </PaginationContent>\n      </Pagination>\n    </div>\n  );\n}\n\ntype DataTableProps<TData extends RowData> = Omit<ComponentProps<'div'>, 'children'> & {\n  /* Each column's header: a string gets a sort button; a function renders its own header. */\n  columns: readonly DataTableColumnDef<TData>[];\n  data: TData[];\n  /** Adds a checkbox column for row selection. */\n  selectable?: boolean;\n  /** Rows per page. 0 shows every row on one page, with no pagination. */\n  pageSize?: number;\n  /** Names pagination when a page contains several tables. */\n  paginationLabel?: string;\n  /** Shows the columns menu, to hide and show columns. */\n  columnToggle?: boolean;\n  /** Shows skeleton rows in place of the data. */\n  loading?: boolean;\n  /** The content of the empty state, when there are no rows. */\n  empty?: ReactNode;\n  /** Controls left of the columns menu, such as a search field. */\n  toolbar?: ReactNode;\n  density?: ComponentProps<typeof Table>['density'];\n  stack?: ComponentProps<typeof Table>['stack'];\n  stackBelow?: ComponentProps<typeof Table>['stackBelow'];\n  stickyFirstColumn?: boolean;\n  surface?: ComponentProps<typeof Table>['surface'];\n  /** Opt in to full-width states without enabling stacked or sticky mode. */\n  statePlacement?: 'table' | 'outside';\n  /** Expanded content shares the same row model in both layouts. */\n  renderSubComponent?: (row: Row<DataTableFeatures, TData>) => ReactNode;\n  head?: ComponentProps<typeof Table>['head'];\n  /**\n   * Any other TanStack option: state and handlers for controlled sorting,\n   * pagination and selection, getRowId, or manualPagination for server data.\n   */\n  options?: Omit<Partial<TableOptions<DataTableFeatures, TData>>, 'data' | 'columns' | 'features'>;\n};\n\nfunction outsideState(outside: boolean | undefined, loading: boolean, rowCount: number) {\n  if (!outside) return undefined;\n  if (loading) return 'loading';\n  return rowCount ? undefined : 'empty';\n}\n\n/*\n * A TanStack Table v9 table on the Table part: sortable headers, pages, an\n * optional selection column, a columns menu, and loading and empty states.\n * Put it on a plain card.\n */\nfunction DataTable<TData extends RowData>({\n  columns,\n  data,\n  selectable = false,\n  pageSize = 10,\n  paginationLabel,\n  columnToggle = false,\n  loading = false,\n  empty,\n  toolbar,\n  density,\n  stack,\n  stackBelow,\n  stickyFirstColumn,\n  surface,\n  statePlacement = 'table',\n  renderSubComponent,\n  head,\n  options,\n  className,\n  ...props\n}: DataTableProps<TData>) {\n  const allColumns = useMemo(\n    () => (selectable ? [selectionColumn<TData>(), ...columns] : columns),\n    [selectable, columns],\n  );\n  const paginated = pageSize > 0;\n  // No selector: the table re-renders on every state change. The rows, the\n  // columns menu and DataTablePagination read state as snapshots and rely on it.\n  const table = useTable({\n    enableRowSelection: selectable,\n    getRowCanExpand: renderSubComponent ? () => true : undefined,\n    ...options,\n    features: dataTableFeatures,\n    data,\n    columns: allColumns,\n    // pageSize 0 shows every row: skip the paginated row model.\n    manualPagination: !paginated || options?.manualPagination,\n    initialState: {\n      pagination: { pageIndex: 0, pageSize: paginated ? pageSize : data.length },\n      ...options?.initialState,\n    },\n  });\n  const loadingRows = paginated ? Math.min(pageSize, 5) : 5;\n  const rows = table.getRowModel().rows;\n  const columnCount = table.getVisibleLeafColumns().length;\n  const outside = stack === 'auto' || stickyFirstColumn || statePlacement === 'outside';\n  const state = outsideState(outside, loading, rows.length);\n  return (\n    <div data-slot=\"data-table\" className={cn('flex flex-col gap-3', className)} {...props}>\n      {toolbar || columnToggle ? (\n        <div data-slot=\"data-table-toolbar\" className=\"flex flex-wrap items-center gap-2\">\n          <div\n            data-slot=\"data-table-toolbar-controls\"\n            className=\"flex min-w-0 flex-1 flex-wrap items-center gap-2\"\n          >\n            {toolbar}\n          </div>\n          {columnToggle ? <DataTableViewOptions table={table} /> : null}\n        </div>\n      ) : null}\n      <Table\n        density={density}\n        head={head}\n        stack={stack}\n        stackBelow={stackBelow}\n        stickyFirstColumn={stickyFirstColumn}\n        surface={surface}\n        state={state}\n        loadingRows={loadingRows}\n        visibleColumns={columnCount}\n        empty={empty}\n        aria-busy={loading || undefined}\n      >\n        <TableHeader>\n          {table.getHeaderGroups().map((group) => (\n            <TableRow key={group.id}>\n              {group.headers.map((header) => (\n                <HeaderCell key={header.id} header={header} stacked={stack === 'auto'} />\n              ))}\n            </TableRow>\n          ))}\n        </TableHeader>\n        <TableBody>\n          <DataTableRows\n            rows={rows}\n            loading={loading}\n            loadingRows={loadingRows}\n            columnCount={columnCount}\n            stacked={stack === 'auto'}\n            empty={empty}\n            renderSubComponent={renderSubComponent}\n          />\n        </TableBody>\n      </Table>\n      {paginated && !loading ? (\n        <DataTablePagination table={table} selectable={selectable} label={paginationLabel} />\n      ) : null}\n    </div>\n  );\n}\n\nexport {\n  DataTable,\n  type DataTableColumnDef,\n  DataTableColumnHeader,\n  type DataTableFeatures,\n  DataTablePagination,\n  DataTableViewOptions,\n  dataTableFeatures,\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "components/ui/data-table-rows.tsx",
      "content": "import { flexRender, type Row, type RowData } from '@tanstack/react-table';\nimport { Fragment, type ReactNode } from 'react';\nimport type { DataTableFeatures } from '@/components/ui/data-table';\nimport { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from '@/components/ui/empty';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { TableCell, TableRow } from '@/components/ui/table';\n\nfunction LoadingRows({ rows, columns }: { rows: number; columns: number }) {\n  return Array.from({ length: rows }, (_, row) => (\n    <TableRow key={`loading-${row}`} data-loading=\"\">\n      {Array.from({ length: columns }, (_, cell) => (\n        <TableCell key={`loading-${row}-${cell}`}>\n          <Skeleton className=\"h-4 w-full rounded-sm\" />\n        </TableCell>\n      ))}\n    </TableRow>\n  ));\n}\n\nfunction DataRow<TData extends RowData>({\n  row,\n  stacked,\n  renderSubComponent,\n}: {\n  row: Row<DataTableFeatures, TData>;\n  stacked: boolean;\n  renderSubComponent?: (row: Row<DataTableFeatures, TData>) => ReactNode;\n}) {\n  const cells = row.getVisibleCells();\n  return (\n    <Fragment>\n      <TableRow data-state={row.getIsSelected() ? 'selected' : undefined}>\n        {cells.map((cell) => {\n          const { meta, header } = cell.column.columnDef;\n          const label = meta?.stackLabel ?? (typeof header === 'string' ? header : cell.column.id);\n          return (\n            <TableCell\n              key={cell.id}\n              stackRole={stacked ? meta?.stackRole : undefined}\n              label={stacked ? label : undefined}\n            >\n              {/*\n               * flexRender renders a cell function as a component. Define columns\n               * outside the render (or memoise them): a new cell function each\n               * render remounts every cell, dropping its state and focus.\n               */}\n              {flexRender(cell.column.columnDef.cell, cell.getContext())}\n            </TableCell>\n          );\n        })}\n      </TableRow>\n      {row.getIsExpanded() && renderSubComponent ? (\n        <TableRow\n          data-slot=\"data-table-expanded-row\"\n          data-state={row.getIsSelected() ? 'selected' : undefined}\n        >\n          <TableCell colSpan={cells.length} className=\"whitespace-normal\">\n            {renderSubComponent(row)}\n          </TableCell>\n        </TableRow>\n      ) : null}\n    </Fragment>\n  );\n}\n\nfunction DataTableRows<TData extends RowData>({\n  rows,\n  loading,\n  loadingRows,\n  columnCount,\n  stacked,\n  empty,\n  renderSubComponent,\n}: {\n  rows: Row<DataTableFeatures, TData>[];\n  loading: boolean;\n  loadingRows: number;\n  columnCount: number;\n  stacked: boolean;\n  empty?: ReactNode;\n  renderSubComponent?: (row: Row<DataTableFeatures, TData>) => ReactNode;\n}) {\n  if (loading) return <LoadingRows rows={loadingRows} columns={columnCount} />;\n  if (rows.length)\n    return rows.map((row) => (\n      <DataRow key={row.id} row={row} stacked={stacked} renderSubComponent={renderSubComponent} />\n    ));\n  return (\n    <TableRow className=\"hover:bg-transparent\">\n      <TableCell colSpan={columnCount} className=\"whitespace-normal\">\n        <Empty size=\"xs\">\n          {empty ?? (\n            <EmptyHeader>\n              <EmptyTitle>No results</EmptyTitle>\n              <EmptyDescription>Rows will show here once there is data.</EmptyDescription>\n            </EmptyHeader>\n          )}\n        </Empty>\n      </TableCell>\n    </TableRow>\n  );\n}\n\nexport { DataTableRows };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}