{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "title": "Data table",
  "description": "A generic TanStack Table with sorting, filtering, column visibility, pagination and optional row selection.",
  "dependencies": [
    "@tanstack/react-table@^9.2.4",
    "cn@^0.4.0",
    "lucide-react@^1.46.0"
  ],
  "registryDependencies": [
    "Familis-co/registry/button",
    "Familis-co/registry/checkbox",
    "Familis-co/registry/input",
    "Familis-co/registry/native-select",
    "Familis-co/registry/dropdown-menu",
    "Familis-co/registry/table",
    "Familis-co/registry/use-data-table"
  ],
  "files": [
    {
      "path": "registry/familis/blocks/data-table/data-table-column-header.tsx",
      "content": "\"use client\"\nimport type { ReactNode } from \"react\"\nimport type { Column, RowData } from \"@tanstack/react-table\"\nimport { ArrowDownIcon, ArrowUpIcon, ArrowUpDownIcon } from \"lucide-react\"\nimport type { DataTableFeatures } from \"@/registry/familis/ui/hooks/use-data-table\"\nimport { Button } from \"@/registry/familis/ui/button\"\n\nexport function DataTableColumnHeader<TData extends RowData, TValue>({\n  column,\n  children,\n}: {\n  column: Column<DataTableFeatures, TData, TValue>\n  children: ReactNode\n}) {\n  if (!column.getCanSort()) return children\n  const Icon =\n    column.getIsSorted() === \"asc\"\n      ? ArrowUpIcon\n      : column.getIsSorted() === \"desc\"\n        ? ArrowDownIcon\n        : ArrowUpDownIcon\n  return (\n    <Button variant=\"ghost\" size=\"sm\" onClick={() => column.toggleSorting()}>\n      {children}\n      <Icon data-icon=\"inline-end\" aria-hidden=\"true\" />\n    </Button>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/familis/blocks/data-table/data-table-content.tsx",
      "content": "\"use client\"\n\nimport type { ReactNode } from \"react\"\nimport type { RowData, ReactTable, Row } from \"@tanstack/react-table\"\nimport type { DataTableFeatures } from \"@/registry/familis/ui/hooks/use-data-table\"\nimport { Checkbox } from \"@/registry/familis/ui/checkbox\"\nimport { DataTableColumnHeader } from \"@/registry/familis/blocks/data-table/data-table-column-header\"\nimport {\n  Table,\n  TableHeader,\n  TableHead,\n  TableBody,\n  TableRow,\n  TableCell,\n} from \"@/registry/familis/ui/table\"\n\nexport function DataTableContent<TData extends RowData>({\n  table,\n  label,\n  loading = false,\n  enableSelection = false,\n  emptyMessage = \"No results found.\",\n  renderRow,\n  wrapRows,\n  leadingHeader,\n}: {\n  table: ReactTable<DataTableFeatures, TData>\n  label: string\n  loading?: boolean\n  enableSelection?: boolean\n  emptyMessage?: ReactNode\n  renderRow?: (row: Row<DataTableFeatures, TData>) => ReactNode\n  wrapRows?: (rows: ReactNode) => ReactNode\n  leadingHeader?: ReactNode\n}) {\n  const visibleCount =\n    table.getVisibleLeafColumns().length +\n    (enableSelection ? 1 : 0) +\n    (leadingHeader !== undefined ? 1 : 0)\n  return (\n    <div className=\"overflow-hidden rounded-lg border\">\n      <Table aria-label={label} aria-busy={loading}>\n        <TableHeader>\n          {table.getHeaderGroups().map((group, groupIndex) => (\n            <TableRow key={group.id}>\n              {leadingHeader !== undefined && groupIndex === 0 ? (\n                <TableHead rowSpan={table.getHeaderGroups().length}>{leadingHeader}</TableHead>\n              ) : null}\n              {enableSelection && groupIndex === 0 && (\n                <TableHead rowSpan={table.getHeaderGroups().length}>\n                  <Checkbox\n                    aria-label=\"Select all rows on this page\"\n                    checked={table.getIsAllPageRowsSelected()}\n                    indeterminate={\n                      table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected()\n                    }\n                    disabled={loading || table.getRowModel().rows.length === 0}\n                    onCheckedChange={(checked) => table.toggleAllPageRowsSelected(checked)}\n                  />\n                </TableHead>\n              )}\n              {group.headers.map((header) => (\n                <TableHead\n                  key={header.id}\n                  colSpan={header.colSpan}\n                  aria-sort={\n                    header.column.getIsSorted() === \"asc\"\n                      ? \"ascending\"\n                      : header.column.getIsSorted() === \"desc\"\n                        ? \"descending\"\n                        : undefined\n                  }\n                >\n                  {header.isPlaceholder ? null : header.column.getCanSort() &&\n                    typeof header.column.columnDef.header === \"string\" ? (\n                    <DataTableColumnHeader column={header.column}>\n                      <table.FlexRender header={header} />\n                    </DataTableColumnHeader>\n                  ) : (\n                    <table.FlexRender header={header} />\n                  )}\n                </TableHead>\n              ))}\n            </TableRow>\n          ))}\n        </TableHeader>\n        <TableBody>\n          {loading || table.getRowModel().rows.length === 0 ? (\n            <TableRow>\n              <TableCell colSpan={Math.max(1, visibleCount)}>\n                <div role=\"status\" className=\"flex min-h-24 items-center justify-center\">\n                  {loading ? \"Loading…\" : emptyMessage}\n                </div>\n              </TableCell>\n            </TableRow>\n          ) : (\n            (() => {\n              const rows = table\n                .getRowModel()\n                .rows.map((row) =>\n                  renderRow ? (\n                    renderRow(row)\n                  ) : (\n                    <DataTableRow\n                      key={row.id}\n                      row={row}\n                      table={table}\n                      enableSelection={enableSelection}\n                    />\n                  ),\n                )\n              return wrapRows ? wrapRows(rows) : rows\n            })()\n          )}\n        </TableBody>\n      </Table>\n    </div>\n  )\n}\n\nexport function DataTableRow<TData extends RowData>({\n  row,\n  table,\n  enableSelection = false,\n  leadingCell,\n  ...props\n}: {\n  row: Row<DataTableFeatures, TData>\n  table: ReactTable<DataTableFeatures, TData>\n  enableSelection?: boolean\n  leadingCell?: ReactNode\n} & Omit<React.ComponentProps<\"tr\">, \"children\">) {\n  return (\n    <TableRow {...props} data-state={row.getIsSelected() ? \"selected\" : undefined}>\n      {leadingCell !== undefined ? <TableCell>{leadingCell}</TableCell> : null}\n      {enableSelection ? (\n        <TableCell>\n          <Checkbox\n            aria-label={`Select row ${row.id}`}\n            checked={row.getIsSelected()}\n            disabled={!row.getCanSelect()}\n            onCheckedChange={(checked) => row.toggleSelected(checked)}\n          />\n        </TableCell>\n      ) : null}\n      {row.getVisibleCells().map((cell) => (\n        <TableCell key={cell.id}>\n          <table.FlexRender cell={cell} />\n        </TableCell>\n      ))}\n    </TableRow>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/familis/blocks/data-table/data-table-pagination.tsx",
      "content": "\"use client\"\n\nimport type { RowData, ReactTable } from \"@tanstack/react-table\"\nimport type { DataTableFeatures } from \"@/registry/familis/ui/hooks/use-data-table\"\nimport { ChevronsLeftIcon, ChevronsRightIcon } from \"lucide-react\"\nimport { Button } from \"@/registry/familis/ui/button\"\nimport { NativeSelect, NativeSelectOption } from \"@/registry/familis/ui/native-select\"\n\nconst defaultPageSizes = [10, 20, 50]\nexport function DataTablePagination<TData extends RowData>({\n  table,\n  enableSelection = false,\n  loading = false,\n  pageSizes = defaultPageSizes,\n}: {\n  table: ReactTable<DataTableFeatures, TData>\n  enableSelection?: boolean\n  loading?: boolean\n  pageSizes?: number[]\n}) {\n  return (\n    <div className=\"flex flex-wrap items-center justify-between gap-3\">\n      <p role=\"status\" className=\"text-sm text-muted-foreground\">\n        {enableSelection && `${table.getFilteredSelectedRowModel().rows.length} selected · `}\n        {table.getRowCount()} rows\n      </p>\n      <div className=\"flex flex-wrap items-center gap-2\">\n        <NativeSelect\n          aria-label=\"Rows per page\"\n          value={table.state.pagination.pageSize}\n          onChange={(event) => table.setPageSize(Number(event.target.value))}\n        >\n          {[...new Set([table.state.pagination.pageSize, ...pageSizes.filter((size) => size > 0)])]\n            .sort((a, b) => a - b)\n            .map((size) => (\n              <NativeSelectOption key={size} value={size}>\n                {size} / page\n              </NativeSelectOption>\n            ))}\n        </NativeSelect>\n        <span className=\"text-sm text-muted-foreground\">\n          Page {table.getPageCount() === 0 ? 0 : table.state.pagination.pageIndex + 1} of{\" \"}\n          {table.getPageCount()}\n        </span>\n        <Button\n          variant=\"outline\"\n          size=\"icon-sm\"\n          aria-label=\"First page\"\n          disabled={loading || !table.getCanPreviousPage()}\n          onClick={() => table.setPageIndex(0)}\n        >\n          <ChevronsLeftIcon aria-hidden=\"true\" />\n        </Button>\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          disabled={loading || !table.getCanPreviousPage()}\n          onClick={() => table.previousPage()}\n        >\n          Previous\n        </Button>\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          disabled={loading || !table.getCanNextPage()}\n          onClick={() => table.nextPage()}\n        >\n          Next\n        </Button>\n        <Button\n          variant=\"outline\"\n          size=\"icon-sm\"\n          aria-label=\"Last page\"\n          disabled={loading || !table.getCanNextPage()}\n          onClick={() => table.setPageIndex(table.getPageCount() - 1)}\n        >\n          <ChevronsRightIcon aria-hidden=\"true\" />\n        </Button>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/familis/blocks/data-table/data-table-toolbar.tsx",
      "content": "\"use client\"\n\nimport type { ReactNode } from \"react\"\nimport type { RowData, ReactTable } from \"@tanstack/react-table\"\nimport type { DataTableFeatures } from \"@/registry/familis/ui/hooks/use-data-table\"\nimport { Input } from \"@/registry/familis/ui/input\"\nimport { Button } from \"@/registry/familis/ui/button\"\nimport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuCheckboxItem,\n} from \"@/registry/familis/ui/dropdown-menu\"\n\nexport function DataTableToolbar<TData extends RowData>({\n  table,\n  filterColumn,\n  filterLabel = \"Filter rows\",\n  children,\n}: {\n  table: ReactTable<DataTableFeatures, TData>\n  filterColumn?: string\n  filterLabel?: string\n  children?: ReactNode\n}) {\n  const filter = filterColumn ? table.getColumn(filterColumn) : undefined\n  return (\n    <div className=\"flex flex-wrap items-center gap-2\">\n      {filter && (\n        <Input\n          type=\"search\"\n          aria-label={filterLabel}\n          placeholder={filterLabel}\n          value={String(filter.getFilterValue() ?? \"\")}\n          onChange={(event) => filter.setFilterValue(event.target.value)}\n          className=\"min-w-40 flex-1\"\n        />\n      )}\n      {children}\n      <DropdownMenu>\n        <DropdownMenuTrigger render={<Button variant=\"outline\" size=\"sm\" />}>\n          Columns\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align=\"end\">\n          <DropdownMenuGroup>\n            {table\n              .getAllLeafColumns()\n              .filter((column) => column.getCanHide())\n              .map((column) => (\n                <DropdownMenuCheckboxItem\n                  key={column.id}\n                  checked={column.getIsVisible()}\n                  disabled={column.getIsVisible() && table.getVisibleLeafColumns().length === 1}\n                  onCheckedChange={(checked) => column.toggleVisibility(checked)}\n                >\n                  {typeof column.columnDef.header === \"string\"\n                    ? column.columnDef.header\n                    : column.id}\n                </DropdownMenuCheckboxItem>\n              ))}\n          </DropdownMenuGroup>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/familis/blocks/data-table/data-table.tsx",
      "content": "\"use client\"\n\nimport { useDataTable } from \"@/registry/familis/ui/hooks/use-data-table\"\n\nimport { useState, type ReactNode } from \"react\"\nimport { cn } from \"cn\"\nimport {\n  type ColumnDef,\n  type RowData,\n  type RowSelectionState,\n  type ReactTable,\n  type Row,\n} from \"@tanstack/react-table\"\nimport { type DataTableFeatures } from \"@/registry/familis/ui/hooks/use-data-table\"\nimport { DataTableToolbar } from \"@/registry/familis/blocks/data-table/data-table-toolbar\"\nimport { DataTablePagination } from \"@/registry/familis/blocks/data-table/data-table-pagination\"\nimport { DataTableContent } from \"@/registry/familis/blocks/data-table/data-table-content\"\n\nexport type DataTableProps<TData extends RowData> = {\n  columns: ColumnDef<DataTableFeatures, TData>[]\n  data: TData[]\n  /** Accessible name of the table. */\n  label: string\n  filterColumn?: string\n  filterLabel?: string\n  pageSize?: number\n  enableSelection?: boolean\n  /** Stable IDs preserve selection when data changes order. */\n  getRowId?: (row: TData, index: number) => string\n  onSelectionChange?: (selection: RowSelectionState) => void\n  emptyMessage?: ReactNode\n  loading?: boolean\n  className?: string\n  renderRow?: (row: Row<DataTableFeatures, TData>) => ReactNode\n  wrapRows?: (rows: ReactNode) => ReactNode\n  leadingHeader?: ReactNode\n  toolbar?: ReactNode | ((table: ReactTable<DataTableFeatures, TData>) => ReactNode)\n  pagination?: ReactNode | ((table: ReactTable<DataTableFeatures, TData>) => ReactNode)\n}\n\nexport function DataTable<TData extends RowData>({\n  columns,\n  data,\n  label,\n  filterColumn,\n  filterLabel = \"Filter rows\",\n  pageSize = 10,\n  enableSelection = false,\n  getRowId,\n  onSelectionChange,\n  emptyMessage = \"No results found.\",\n  loading = false,\n  className,\n  toolbar,\n  pagination,\n  renderRow,\n  wrapRows,\n  leadingHeader,\n}: DataTableProps<TData>) {\n  const [selection, setSelection] = useState<RowSelectionState>({})\n  const table = useDataTable({\n    columns,\n    data,\n    getRowId,\n    enableRowSelection: enableSelection,\n    initialState: { pagination: { pageIndex: 0, pageSize: Math.max(1, pageSize) } },\n    state: { rowSelection: selection },\n    onRowSelectionChange: (updater) => {\n      const next = typeof updater === \"function\" ? updater(selection) : updater\n      setSelection(next)\n      onSelectionChange?.(next)\n    },\n  })\n  return (\n    <DataTableView\n      table={table}\n      label={label}\n      filterColumn={filterColumn}\n      filterLabel={filterLabel}\n      enableSelection={enableSelection}\n      emptyMessage={emptyMessage}\n      loading={loading}\n      className={className}\n      toolbar={toolbar}\n      renderRow={renderRow}\n      wrapRows={wrapRows}\n      leadingHeader={leadingHeader}\n      pagination={pagination}\n    />\n  )\n}\n\nexport type DataTableViewProps<TData extends RowData> = Omit<\n  DataTableProps<TData>,\n  \"columns\" | \"data\" | \"getRowId\" | \"onSelectionChange\" | \"pageSize\"\n> & {\n  table: ReactTable<DataTableFeatures, TData>\n}\n\n/** Compose your own toolbar/footer, or use the supplied defaults. Null hides a slot. */\nexport function DataTableView<TData extends RowData>({\n  table,\n  label,\n  filterColumn,\n  filterLabel,\n  enableSelection = false,\n  emptyMessage,\n  loading = false,\n  className,\n  toolbar,\n  pagination,\n  renderRow,\n  wrapRows,\n  leadingHeader,\n}: DataTableViewProps<TData>) {\n  return (\n    <div className={cn(\"flex w-full min-w-0 flex-col gap-4\", className)}>\n      {toolbar === undefined ? (\n        <DataTableToolbar table={table} filterColumn={filterColumn} filterLabel={filterLabel} />\n      ) : typeof toolbar === \"function\" ? (\n        toolbar(table)\n      ) : (\n        toolbar\n      )}\n      <DataTableContent\n        table={table}\n        label={label}\n        loading={loading}\n        enableSelection={enableSelection}\n        renderRow={renderRow}\n        wrapRows={wrapRows}\n        leadingHeader={leadingHeader}\n        emptyMessage={emptyMessage}\n      />\n      {pagination === undefined ? (\n        <DataTablePagination table={table} enableSelection={enableSelection} loading={loading} />\n      ) : typeof pagination === \"function\" ? (\n        pagination(table)\n      ) : (\n        pagination\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "base": "base",
    "storybookId": "ui-blocks-data-table--default"
  },
  "type": "registry:block"
}