{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "title": "Data Table",
  "description": "A table with sortable headers, a text filter, pagination and row selection, built on TanStack Table v9.",
  "dependencies": [
    "@tanstack/react-table@^9.2.4"
  ],
  "registryDependencies": [
    "@corsair-ui/utils",
    "@corsair-ui/table",
    "@corsair-ui/pagination",
    "@corsair-ui/checkbox",
    "@corsair-ui/input"
  ],
  "files": [
    {
      "path": "registry/default/ui/data-table.tsx",
      "content": "\"use client\";\n\nimport {\n  columnFilteringFeature,\n  createFilteredRowModel,\n  createPaginatedRowModel,\n  createSortedRowModel,\n  filterFn_includesString,\n  functionalUpdate,\n  globalFilteringFeature,\n  rowPaginationFeature,\n  rowSelectionFeature,\n  rowSortingFeature,\n  sortFn_alphanumeric,\n  sortFn_basic,\n  sortFn_datetime,\n  sortFn_text,\n  tableFeatures,\n  useTable,\n  type ColumnDef,\n  type PaginationState,\n  type RowData,\n  type RowSelectionState,\n  type SortingState,\n  type Updater,\n} from \"@tanstack/react-table\";\nimport { useMemo, useState, type ComponentProps, type ReactNode } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport { Checkbox } from \"@/registry/default/ui/checkbox\";\nimport { Input } from \"@/registry/default/ui/input\";\nimport {\n  getPaginationRange,\n  Pagination,\n  PaginationContent,\n  PaginationEllipsis,\n  PaginationItem,\n  PaginationLink,\n  PaginationNext,\n  PaginationPrevious,\n} from \"@/registry/default/ui/pagination\";\nimport {\n  Table,\n  TableBody,\n  TableCaption,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n  TableSortButton,\n} from \"@/registry/default/ui/table\";\n\n/**\n * The TanStack Table features the data table registers: sorting, a global\n * filter, pagination and row selection. Use its type to build typed columns:\n * `createColumnHelper<typeof dataTableFeatures, Payment>()`.\n */\nconst dataTableFeatures = tableFeatures({\n  columnFilteringFeature,\n  globalFilteringFeature,\n  rowSortingFeature,\n  rowPaginationFeature,\n  rowSelectionFeature,\n  filteredRowModel: createFilteredRowModel(),\n  sortedRowModel: createSortedRowModel(),\n  paginatedRowModel: createPaginatedRowModel(),\n  filterFns: { includesString: filterFn_includesString },\n  sortFns: {\n    alphanumeric: sortFn_alphanumeric,\n    basic: sortFn_basic,\n    datetime: sortFn_datetime,\n    text: sortFn_text,\n  },\n});\n\ntype DataTableFeatures = typeof dataTableFeatures;\n\n/** Which rows are selected, by row id. `false` and missing ids both mean \"not selected\". */\ntype DataTableRowSelection = Record<string, boolean>;\n\n/**\n * A column of the data table: a TanStack Table v9 `ColumnDef` from\n * `@tanstack/react-table`, bound to the data table's features. Read a typed\n * value in a cell with `getValue<number>()`, or build the columns with\n * `createColumnHelper<typeof dataTableFeatures, TData>().columns([...])`.\n */\ntype DataTableColumnDef<TData extends RowData> = ColumnDef<DataTableFeatures, TData, unknown>;\n\ninterface DataTableLabels<TData> {\n  /** Accessible name of the header checkbox. */\n  selectAll: string;\n  /** Accessible name of a row checkbox. Gets the row's 1-based position in the current order and its data. */\n  selectRow: (position: number, row: TData) => string;\n  /** Announced when the selection changes. Counts only rows that pass the filter. */\n  selectedCount: (selected: number, total: number) => string;\n  /** Announced when the filter changes the number of rows. */\n  rowCount: (count: number) => string;\n  /** Name of the pagination landmark. */\n  pagination: string;\n  /** Visible text of the previous-page button, shown from `sm` up. */\n  previous: string;\n  /** Accessible name of the previous-page button. */\n  previousPage: string;\n  /** Visible text of the next-page button, shown from `sm` up. */\n  next: string;\n  /** Accessible name of the next-page button. */\n  nextPage: string;\n  /** Accessible name of a page number button. */\n  page: (page: number) => string;\n  /** Read in place of the dots that stand for skipped pages. */\n  morePages: string;\n}\n\nconst DEFAULT_LABELS: DataTableLabels<unknown> = {\n  selectAll: \"Select all rows\",\n  selectRow: (position) => `Select row ${position}`,\n  selectedCount: (selected, total) => `${selected} of ${total} selected`,\n  rowCount: (count) => (count === 1 ? \"1 row\" : `${count} rows`),\n  pagination: \"Pagination\",\n  previous: \"Previous\",\n  previousPage: \"Go to previous page\",\n  next: \"Next\",\n  nextPage: \"Go to next page\",\n  page: (page) => `Page ${page}`,\n  morePages: \"More pages\",\n};\n\ninterface DataTableProps<TData extends RowData> extends Omit<ComponentProps<\"div\">, \"children\"> {\n  /** Column definitions (TanStack Table v9 `ColumnDef`s from `@tanstack/react-table`). */\n  columns: ReadonlyArray<DataTableColumnDef<TData>>;\n  /** The rows. Keep the array stable between renders (state, memo or module scope). */\n  data: ReadonlyArray<TData>;\n  /** A stable id per row, used as the key of `rowSelection`. Defaults to the row's index in `data`. */\n  getRowId?: (row: TData, index: number) => string;\n  /** Sort by clicking the header of any column whose `header` is a string (unless the column sets `enableSorting: false`). */\n  enableSorting?: boolean;\n  /** Adds a column of checkboxes to select rows, with a select-all checkbox in the header. */\n  enableRowSelection?: boolean;\n  /** Selected rows by id (controlled). */\n  rowSelection?: DataTableRowSelection;\n  /** Selected rows at first (uncontrolled). */\n  defaultRowSelection?: DataTableRowSelection;\n  /** Called with the new selection whenever it changes. */\n  onRowSelectionChange?: (rowSelection: DataTableRowSelection) => void;\n  /** Text of the filter input (controlled). Rows match when any column's value contains it. */\n  globalFilter?: string;\n  /** Text of the filter input at first (uncontrolled). */\n  defaultGlobalFilter?: string;\n  /** Called with the new filter text on every change. */\n  onGlobalFilterChange?: (globalFilter: string) => void;\n  /** Placeholder of the filter input. */\n  filterPlaceholder?: string;\n  /** Accessible name of the filter input. `null` hides the input. */\n  filterLabel?: string | null;\n  /** Rows per page. `null` shows every row without pagination. */\n  pageSize?: number | null;\n  /** Shown in a single cell when no row matches. */\n  emptyMessage?: ReactNode;\n  /** Names the table. A string is read by screen readers only; any other node is shown below the table. */\n  caption?: ReactNode;\n  /** Text for screen readers and the accessible names of controls. */\n  labels?: Partial<DataTableLabels<TData>>;\n  /** Extra controls rendered beside the filter input: column toggles, buttons, menus. */\n  toolbar?: ReactNode;\n  /** Names the scroll container of the table, making it a focusable region (see `Table`). */\n  scrollLabel?: string;\n}\n\nconst EMPTY_SELECTION: RowSelectionState = {};\n\n/** TanStack's selection keeps only `true` entries. */\nfunction toTableSelection(selection: DataTableRowSelection | undefined): RowSelectionState {\n  if (!selection) return EMPTY_SELECTION;\n  const next: RowSelectionState = {};\n  for (const [id, selected] of Object.entries(selection)) if (selected) next[id] = true;\n  return next;\n}\n\n/**\n * A table with sorting, a text filter, pagination and row selection, built\n * on TanStack Table v9 and rendered with `Table`, `Pagination`, `Checkbox`\n * and `Input`. Pass `columns` (`ColumnDef`s from `@tanstack/react-table`;\n * `DataTableColumnDef` is the matching type) and `data`.\n *\n * Columns whose `header` is a string get a sort button: Tab reaches it,\n * Enter or Space sorts ascending, then descending, and the header's\n * `aria-sort` tells screen readers the direction. The filter is a search\n * input named by `filterLabel`. With `enableRowSelection`, each row has a\n * named checkbox, the header checkbox selects every row that passes the\n * filter (and shows a dash when only some are), and selected rows get\n * `data-state=\"selected\"`. A visually hidden status region announces the\n * selected count and the number of matching rows. The page buttons are\n * native buttons in a `Pagination` landmark; Previous and Next stay\n * focusable at the ends and are marked `aria-disabled`. Nothing animates\n * beyond colour transitions, which reduced motion turns off.\n *\n * @example\n * const columns: DataTableColumnDef<Payment>[] = [\n *   { accessorKey: \"email\", header: \"Email\" },\n *   { accessorKey: \"amount\", header: \"Amount\", cell: ({ getValue }) => format(getValue()) },\n * ];\n *\n * <DataTable columns={columns} data={payments} getRowId={(row) => row.id} enableRowSelection />\n */\nfunction DataTable<TData extends RowData>({\n  columns,\n  data,\n  getRowId,\n  enableSorting = true,\n  enableRowSelection = false,\n  rowSelection,\n  defaultRowSelection,\n  onRowSelectionChange,\n  globalFilter,\n  defaultGlobalFilter = \"\",\n  onGlobalFilterChange,\n  filterPlaceholder = \"Filter…\",\n  filterLabel = \"Filter rows\",\n  pageSize = 10,\n  emptyMessage = \"No results.\",\n  caption,\n  labels: labelsProp,\n  toolbar,\n  scrollLabel,\n  className,\n  ...props\n}: DataTableProps<TData>) {\n  const labels = { ...(DEFAULT_LABELS as DataTableLabels<TData>), ...labelsProp };\n\n  const [sorting, setSorting] = useState<SortingState>([]);\n  const [pageIndex, setPageIndex] = useState(0);\n  const [internalFilter, setInternalFilter] = useState(defaultGlobalFilter);\n  const [internalSelection, setInternalSelection] = useState<DataTableRowSelection>(\n    defaultRowSelection ?? {}\n  );\n\n  const filter = globalFilter !== undefined ? globalFilter : internalFilter;\n  const plainSelection = rowSelection !== undefined ? rowSelection : internalSelection;\n  const selection = useMemo(() => toTableSelection(plainSelection), [plainSelection]);\n  const size = pageSize ?? 10;\n  const pagination: PaginationState = { pageIndex, pageSize: size };\n\n  const setFilter = (next: string) => {\n    if (globalFilter === undefined) setInternalFilter(next);\n    onGlobalFilterChange?.(next);\n  };\n\n  const table = useTable({\n    features: dataTableFeatures,\n    columns,\n    data,\n    getRowId,\n    enableSorting,\n    enableRowSelection,\n    enableRowRangeSelection: false,\n    state: { sorting, globalFilter: filter, rowSelection: selection, pagination },\n    onSortingChange: (updater: Updater<SortingState>) =>\n      setSorting((previous) => functionalUpdate(updater, previous)),\n    onGlobalFilterChange: (updater: Updater<string>) =>\n      setFilter(functionalUpdate(updater, filter)),\n    onRowSelectionChange: (updater: Updater<RowSelectionState>) => {\n      const next: DataTableRowSelection = { ...functionalUpdate(updater, selection) };\n      if (rowSelection === undefined) setInternalSelection(next);\n      onRowSelectionChange?.(next);\n    },\n    onPaginationChange: (updater: Updater<PaginationState>) =>\n      setPageIndex(\n        (previous) => functionalUpdate(updater, { pageIndex: previous, pageSize: size }).pageIndex\n      ),\n  });\n\n  const filteredRows = table.getFilteredRowModel().rows;\n  const rows = pageSize === null ? table.getPrePaginatedRowModel().rows : table.getRowModel().rows;\n  const pageCount = pageSize === null ? 1 : table.getPageCount();\n  const currentPage = Math.min(pageIndex, Math.max(0, pageCount - 1));\n  const firstPosition = pageSize === null ? 1 : currentPage * size + 1;\n  const columnCount = table.getAllLeafColumns().length + (enableRowSelection ? 1 : 0);\n  const headerGroups = table.getHeaderGroups();\n\n  const selectable = enableRowSelection ? filteredRows.filter((row) => row.getCanSelect()) : [];\n  const selectedCount = selectable.filter((row) => row.getIsSelected()).length;\n  const allSelected = selectable.length > 0 && selectedCount === selectable.length;\n  const toggleAll = () =>\n    table.setRowSelection((previous) => {\n      const next: RowSelectionState = { ...previous };\n      for (const row of selectable) {\n        if (allSelected) delete next[row.id];\n        else next[row.id] = true;\n      }\n      return next;\n    });\n\n  const status = [\n    enableRowSelection ? labels.selectedCount(selectedCount, filteredRows.length) : null,\n    labels.rowCount(filteredRows.length),\n  ]\n    .filter(Boolean)\n    .join(\". \");\n\n  const showToolbar = filterLabel !== null || toolbar !== undefined;\n  const canPrevious = table.getCanPreviousPage();\n  const canNext = table.getCanNextPage();\n\n  return (\n    <div data-slot=\"data-table\" className={cn(\"flex flex-col gap-4\", className)} {...props}>\n      {showToolbar ? (\n        <div data-slot=\"data-table-toolbar\" className=\"flex flex-wrap items-center gap-2\">\n          {filterLabel !== null ? (\n            <Input\n              type=\"search\"\n              data-slot=\"data-table-filter\"\n              aria-label={filterLabel}\n              placeholder={filterPlaceholder}\n              value={filter}\n              onChange={(event) => table.setGlobalFilter(event.target.value)}\n              className=\"max-w-xs\"\n            />\n          ) : null}\n          {toolbar}\n        </div>\n      ) : null}\n      <div data-slot=\"data-table-body\" className=\"rounded-md border\">\n        <Table scrollLabel={scrollLabel}>\n          {caption === undefined || caption === null ? null : (\n            <TableCaption className={cn(typeof caption === \"string\" && \"sr-only\")}>\n              {caption}\n            </TableCaption>\n          )}\n          <TableHeader>\n            {headerGroups.map((group, groupIndex) => (\n              <TableRow key={group.id}>\n                {enableRowSelection && groupIndex === 0 ? (\n                  <TableHead rowSpan={headerGroups.length} className=\"w-10 pl-3\">\n                    <Checkbox\n                      data-slot=\"data-table-select-all\"\n                      aria-label={labels.selectAll}\n                      checked={allSelected ? true : selectedCount > 0 ? \"indeterminate\" : false}\n                      disabled={selectable.length === 0}\n                      onCheckedChange={toggleAll}\n                      className=\"align-middle\"\n                    />\n                  </TableHead>\n                ) : null}\n                {group.headers.map((header) => {\n                  const { column } = header;\n                  const title = column.columnDef.header;\n                  const sortable =\n                    !header.isPlaceholder &&\n                    header.subHeaders.length === 0 &&\n                    typeof title === \"string\" &&\n                    column.getCanSort();\n                  const direction = column.getIsSorted();\n                  return (\n                    <TableHead\n                      key={header.id}\n                      colSpan={header.colSpan}\n                      sort={sortable ? direction : undefined}\n                    >\n                      {header.isPlaceholder ? null : sortable ? (\n                        <TableSortButton\n                          direction={direction}\n                          onSort={(next) => column.toggleSorting(next === \"desc\")}\n                        >\n                          {title}\n                        </TableSortButton>\n                      ) : (\n                        <table.FlexRender header={header} />\n                      )}\n                    </TableHead>\n                  );\n                })}\n              </TableRow>\n            ))}\n          </TableHeader>\n          <TableBody>\n            {rows.length ? (\n              rows.map((row, index) => {\n                const selected = row.getIsSelected();\n                return (\n                  <TableRow\n                    key={row.id}\n                    data-state={enableRowSelection && selected ? \"selected\" : undefined}\n                  >\n                    {enableRowSelection ? (\n                      <TableCell className=\"w-10 pl-3\">\n                        <Checkbox\n                          data-slot=\"data-table-select-row\"\n                          aria-label={labels.selectRow(firstPosition + index, row.original)}\n                          checked={selected}\n                          disabled={!row.getCanSelect()}\n                          onCheckedChange={(checked) => row.toggleSelected(checked === true)}\n                          className=\"align-middle\"\n                        />\n                      </TableCell>\n                    ) : null}\n                    {row.getAllCells().map((cell) => (\n                      <TableCell key={cell.id}>\n                        <table.FlexRender cell={cell} />\n                      </TableCell>\n                    ))}\n                  </TableRow>\n                );\n              })\n            ) : (\n              <TableRow data-slot=\"data-table-empty\">\n                <TableCell colSpan={columnCount} className=\"text-muted-foreground h-24 text-center\">\n                  {emptyMessage}\n                </TableCell>\n              </TableRow>\n            )}\n          </TableBody>\n        </Table>\n      </div>\n      <div role=\"status\" data-slot=\"data-table-status\" className=\"sr-only\">\n        {status}\n      </div>\n      {pageSize !== null && pageCount > 1 ? (\n        <Pagination\n          aria-label={labels.pagination}\n          data-slot=\"data-table-pagination\"\n          className=\"justify-end\"\n        >\n          <PaginationContent>\n            <PaginationItem>\n              <PaginationPrevious asChild label={labels.previous} aria-label={labels.previousPage}>\n                <button\n                  type=\"button\"\n                  aria-disabled={!canPrevious || undefined}\n                  onClick={() => {\n                    if (canPrevious) table.previousPage();\n                  }}\n                />\n              </PaginationPrevious>\n            </PaginationItem>\n            {getPaginationRange({ page: currentPage + 1, pageCount }).map((entry, index) => (\n              <PaginationItem key={entry === \"ellipsis\" ? `ellipsis-${index}` : entry}>\n                {entry === \"ellipsis\" ? (\n                  <PaginationEllipsis label={labels.morePages} />\n                ) : (\n                  <PaginationLink\n                    asChild\n                    isActive={entry === currentPage + 1}\n                    aria-label={labels.page(entry)}\n                  >\n                    <button type=\"button\" onClick={() => table.setPageIndex(entry - 1)}>\n                      {entry}\n                    </button>\n                  </PaginationLink>\n                )}\n              </PaginationItem>\n            ))}\n            <PaginationItem>\n              <PaginationNext asChild label={labels.next} aria-label={labels.nextPage}>\n                <button\n                  type=\"button\"\n                  aria-disabled={!canNext || undefined}\n                  onClick={() => {\n                    if (canNext) table.nextPage();\n                  }}\n                />\n              </PaginationNext>\n            </PaginationItem>\n          </PaginationContent>\n        </Pagination>\n      ) : null}\n    </div>\n  );\n}\n\nexport {\n  DataTable,\n  dataTableFeatures,\n  type DataTableColumnDef,\n  type DataTableFeatures,\n  type DataTableLabels,\n  type DataTableProps,\n  type DataTableRowSelection,\n};\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}