{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "type": "registry:ui",
  "title": "Data Table",
  "description": "A full data table on TanStack Table: sorting, selection, search, column visibility, pagination, loading and empty states. Pass `manual` for server-side data.",
  "dependencies": [
    "@tanstack/react-table@^8.21.3",
    "tailwind-variants@^1.0.0"
  ],
  "registryDependencies": [
    "https://junco.madebytobi.com/r/button.json",
    "https://junco.madebytobi.com/r/checkbox.json",
    "https://junco.madebytobi.com/r/dropdown-menu.json",
    "https://junco.madebytobi.com/r/empty-state.json",
    "https://junco.madebytobi.com/r/icons.json",
    "https://junco.madebytobi.com/r/input.json",
    "https://junco.madebytobi.com/r/junco-theme.json",
    "https://junco.madebytobi.com/r/pagination.json",
    "https://junco.madebytobi.com/r/select.json",
    "https://junco.madebytobi.com/r/skeleton.json",
    "https://junco.madebytobi.com/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/junco/ui/data-table.tsx",
      "type": "registry:ui",
      "content": "import * as React from \"react\";\nimport {\n  flexRender,\n  getCoreRowModel,\n  getFilteredRowModel,\n  getPaginationRowModel,\n  getSortedRowModel,\n  useReactTable,\n  type ColumnDef,\n  type PaginationState,\n  type RowSelectionState,\n  type SortingState,\n  type Table as TanstackTable,\n  type TableOptions,\n  type VisibilityState,\n} from \"@tanstack/react-table\";\nimport { tv } from \"tailwind-variants\";\nimport { Button } from \"@/components/ui/button\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuLabel,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { EmptyState } from \"@/components/ui/empty-state\";\nimport { Input } from \"@/components/ui/input\";\nimport { Pagination } from \"@/components/ui/pagination\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { cn } from \"@/lib/utils\";\nimport { ArrowDownIcon, ArrowUpDownIcon, ArrowUpIcon, InboxIcon, SearchIcon } from \"@/lib/junco/icons\";\n\nexport const dataTableVariants = tv({\n  slots: {\n    root: \"grid gap-3\",\n    toolbar: \"flex flex-wrap items-center gap-2\",\n    container: \"relative w-full overflow-auto rounded-card border border-border bg-surface\",\n    table: \"w-full caption-bottom border-separate border-spacing-0 text-sm\",\n    thead: \"\",\n    headerCell: [\n      \"h-10 border-b border-border bg-surface px-3 text-left align-middle text-xs font-medium whitespace-nowrap text-muted-foreground\",\n      \"first:pl-4 last:pr-4\",\n    ],\n    row: \"transition-colors duration-fast ease-standard hover:bg-muted/50 data-[state=selected]:bg-accent\",\n    cell: \"border-b border-border px-3 py-2.5 align-middle first:pl-4 last:pr-4 [tr:last-child>&]:border-b-0\",\n    footer: \"flex flex-wrap items-center justify-between gap-3 text-sm text-muted-foreground\",\n  },\n  variants: {\n    density: {\n      compact: { cell: \"py-1.5\", headerCell: \"h-9\" },\n      normal: {},\n      relaxed: { cell: \"py-3.5\", headerCell: \"h-11\" },\n    },\n    isStickyHeader: {\n      true: { headerCell: \"sticky top-0 z-sticky\" },\n    },\n  },\n  defaultVariants: { density: \"normal\", isStickyHeader: true },\n});\n\ntype Slots = keyof ReturnType<typeof dataTableVariants>;\n\nexport interface DataTableProps<TData> {\n  columns: ColumnDef<TData, any>[];\n  data: TData[];\n  /** Stable row ids (recommended with selection). Defaults to the row index. */\n  getRowId?: (row: TData, index: number) => string;\n\n  /** Shows skeleton rows and sets aria-busy. */\n  isLoading?: boolean;\n  /** Skeleton rows while loading. Defaults to the page size (max 10). */\n  loadingRows?: number;\n\n  /** Adds a checkbox column. */\n  enableRowSelection?: boolean | ((row: TData) => boolean);\n  rowSelection?: RowSelectionState;\n  onRowSelectionChange?: (selection: RowSelectionState) => void;\n\n  /** Shows a search box that filters across all columns. */\n  enableGlobalFilter?: boolean;\n  globalFilter?: string;\n  onGlobalFilterChange?: (value: string) => void;\n  searchPlaceholder?: string;\n\n  /** Shows a \"Columns\" menu to hide and show columns. */\n  enableColumnVisibility?: boolean;\n  columnVisibility?: VisibilityState;\n  onColumnVisibilityChange?: (visibility: VisibilityState) => void;\n\n  sorting?: SortingState;\n  onSortingChange?: (sorting: SortingState) => void;\n  defaultSorting?: SortingState;\n\n  /** Rows per page. Set `false` to show every row. Default 10. */\n  pageSize?: number | false;\n  pageSizeOptions?: number[];\n  pagination?: PaginationState;\n  onPaginationChange?: (pagination: PaginationState) => void;\n  /** Server-side data: sorting, filtering and paging happen outside; pass the total page count. */\n  manual?: boolean;\n  pageCount?: number;\n  /** Total rows on the server, for the footer when `manual`. */\n  rowCount?: number;\n\n  /** Shown when there are no rows. */\n  emptyState?: React.ReactNode;\n  /** Extra controls on the right of the toolbar (filters, buttons). */\n  toolbar?: React.ReactNode;\n  /** Accessible table name, also shown visually hidden as the caption. */\n  caption?: string;\n  /** Max height of the scroll area, e.g. \"24rem\". The header stays visible while scrolling. */\n  maxHeight?: string;\n  density?: \"compact\" | \"normal\" | \"relaxed\";\n  isStickyHeader?: boolean;\n  /** Escape hatch for anything else TanStack Table supports. */\n  tableOptions?: Partial<TableOptions<TData>>;\n  /** Receives the table instance (for custom toolbars or bulk actions). */\n  onTableReady?: (table: TanstackTable<TData>) => void;\n  className?: string;\n  classNames?: Partial<Record<Slots, string>>;\n}\n\nfunction useControllable<T>(value: T | undefined, onChange: ((v: T) => void) | undefined, initial: T) {\n  const [inner, setInner] = React.useState<T>(initial);\n  const current = value ?? inner;\n  const set = React.useCallback(\n    (updater: T | ((old: T) => T)) => {\n      const next = typeof updater === \"function\" ? (updater as (old: T) => T)(current) : updater;\n      if (value === undefined) setInner(next);\n      onChange?.(next);\n    },\n    [current, value, onChange],\n  );\n  return [current, set] as const;\n}\n\nfunction SortIcon({ dir }: { dir: false | \"asc\" | \"desc\" }) {\n  if (dir === \"asc\") return <ArrowUpIcon aria-hidden className=\"size-3.5\" />;\n  if (dir === \"desc\") return <ArrowDownIcon aria-hidden className=\"size-3.5\" />;\n  return <ArrowUpDownIcon aria-hidden className=\"size-3.5 opacity-50\" />;\n}\n\nfunction headerText(column: { columnDef: { header?: unknown }; id: string }) {\n  return typeof column.columnDef.header === \"string\" ? column.columnDef.header : column.id;\n}\n\n/**\n * A full data table on TanStack Table: sorting, selection, search, column visibility, pagination,\n * loading and empty states. Pass `manual` for server-side data.\n *\n *   import { DataTable, type ColumnDef } from \"junco/data-table\";\n */\nexport function DataTable<TData>({\n  columns,\n  data,\n  getRowId,\n  isLoading = false,\n  loadingRows,\n  enableRowSelection = false,\n  rowSelection: rowSelectionProp,\n  onRowSelectionChange,\n  enableGlobalFilter = false,\n  globalFilter: globalFilterProp,\n  onGlobalFilterChange,\n  searchPlaceholder = \"Search…\",\n  enableColumnVisibility = false,\n  columnVisibility: columnVisibilityProp,\n  onColumnVisibilityChange,\n  sorting: sortingProp,\n  onSortingChange,\n  defaultSorting = [],\n  pageSize = 10,\n  pageSizeOptions = [10, 20, 50],\n  pagination: paginationProp,\n  onPaginationChange,\n  manual = false,\n  pageCount,\n  rowCount,\n  emptyState,\n  toolbar,\n  caption,\n  maxHeight,\n  density,\n  isStickyHeader,\n  tableOptions,\n  onTableReady,\n  className,\n  classNames,\n}: DataTableProps<TData>) {\n  const styles = dataTableVariants({ density, isStickyHeader });\n  const pageSizeLabelId = `${React.useId()}-page-size`;\n  const paged = pageSize !== false;\n\n  const [sorting, setSorting] = useControllable(sortingProp, onSortingChange, defaultSorting);\n  const [rowSelection, setRowSelection] = useControllable(rowSelectionProp, onRowSelectionChange, {});\n  const [globalFilter, setGlobalFilter] = useControllable(globalFilterProp, onGlobalFilterChange, \"\");\n  const [columnVisibility, setColumnVisibility] = useControllable(columnVisibilityProp, onColumnVisibilityChange, {});\n  const [pagination, setPagination] = useControllable(paginationProp, onPaginationChange, {\n    pageIndex: 0,\n    pageSize: paged ? pageSize : Number.MAX_SAFE_INTEGER,\n  });\n  // The current size is always a choice, so a pageSize outside the options never shows a blank select.\n  const sizeOptions = paged ? [...new Set([...pageSizeOptions, pagination.pageSize])].sort((a, b) => a - b) : pageSizeOptions;\n\n  const allColumns = React.useMemo<ColumnDef<TData, any>[]>(() => {\n    if (!enableRowSelection) return columns;\n    const select: ColumnDef<TData, any> = {\n      id: \"__select\",\n      enableSorting: false,\n      enableHiding: false,\n      enableGlobalFilter: false,\n      header: ({ table }) => (\n        <Checkbox\n          aria-label=\"Select all rows on this page\"\n          checked={table.getIsAllPageRowsSelected() ? true : table.getIsSomePageRowsSelected() ? \"indeterminate\" : false}\n          onCheckedChange={(v) => table.toggleAllPageRowsSelected(v === true)}\n        />\n      ),\n      cell: ({ row }) => (\n        <Checkbox\n          aria-label=\"Select row\"\n          checked={row.getIsSelected()}\n          disabled={!row.getCanSelect()}\n          onCheckedChange={(v) => row.toggleSelected(v === true)}\n        />\n      ),\n      meta: { className: \"w-10\" },\n    };\n    return [select, ...columns];\n  }, [columns, enableRowSelection]);\n\n  const table = useReactTable<TData>({\n    data,\n    columns: allColumns,\n    getRowId,\n    state: { sorting, rowSelection, globalFilter, columnVisibility, pagination },\n    enableRowSelection:\n      typeof enableRowSelection === \"function\" ? (row) => enableRowSelection(row.original) : enableRowSelection,\n    onSortingChange: setSorting,\n    onRowSelectionChange: setRowSelection,\n    onGlobalFilterChange: setGlobalFilter,\n    onColumnVisibilityChange: setColumnVisibility,\n    onPaginationChange: setPagination,\n    getCoreRowModel: getCoreRowModel(),\n    ...(manual\n      ? { manualSorting: true, manualFiltering: true, manualPagination: true, pageCount: pageCount ?? -1, rowCount }\n      : {\n          getSortedRowModel: getSortedRowModel(),\n          getFilteredRowModel: getFilteredRowModel(),\n          getPaginationRowModel: paged ? getPaginationRowModel() : undefined,\n        }),\n    autoResetPageIndex: false,\n    // TanStack sorts numbers descending first and text ascending first; one rule is easier to predict.\n    sortDescFirst: false,\n    ...tableOptions,\n  });\n\n  React.useEffect(() => {\n    onTableReady?.(table);\n  }, [onTableReady, table]);\n\n  // When filtering shrinks the data, don't leave the user on a page that no longer exists.\n  const totalPages = Math.max(1, table.getPageCount());\n  React.useEffect(() => {\n    if (paged && pagination.pageIndex > totalPages - 1) setPagination({ ...pagination, pageIndex: totalPages - 1 });\n  }, [paged, pagination, totalPages, setPagination]);\n\n  const rows = table.getRowModel().rows;\n  const visibleColumns = table.getVisibleLeafColumns();\n  const skeletonCount = loadingRows ?? Math.min(paged ? pagination.pageSize : 5, 10);\n  const selectedCount = Object.keys(rowSelection).filter((k) => rowSelection[k]).length;\n  const filteredCount = manual ? (rowCount ?? data.length) : table.getFilteredRowModel().rows.length;\n  const hideableColumns = table.getAllLeafColumns().filter((c) => c.getCanHide());\n  const showToolbar = enableGlobalFilter || enableColumnVisibility || toolbar;\n\n  return (\n    <div data-slot=\"data-table\" className={styles.root({ className: cn(classNames?.root, className) })}>\n      {showToolbar && (\n        <div className={styles.toolbar({ className: classNames?.toolbar })}>\n          {enableGlobalFilter && (\n            <div className=\"relative w-full max-w-xs\">\n              <SearchIcon aria-hidden className=\"pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground\" />\n              <Input\n                type=\"search\"\n                aria-label=\"Search table\"\n                placeholder={searchPlaceholder}\n                value={globalFilter}\n                onChange={(e) => setGlobalFilter(e.target.value)}\n                className=\"pl-8\"\n              />\n            </div>\n          )}\n          <div className=\"ml-auto flex items-center gap-2\">\n            {toolbar}\n            {enableColumnVisibility && hideableColumns.length > 0 && (\n              <DropdownMenu>\n                <DropdownMenuTrigger asChild>\n                  <Button variant=\"outline\">Columns</Button>\n                </DropdownMenuTrigger>\n                <DropdownMenuContent align=\"end\" className=\"min-w-44\">\n                  <DropdownMenuLabel>Show columns</DropdownMenuLabel>\n                  {hideableColumns.map((column) => (\n                    <DropdownMenuCheckboxItem\n                      key={column.id}\n                      checked={column.getIsVisible()}\n                      onCheckedChange={(v) => column.toggleVisibility(v === true)}\n                      // keep the menu open so several columns can be toggled in a row\n                      onSelect={(e) => e.preventDefault()}\n                    >\n                      {headerText(column)}\n                    </DropdownMenuCheckboxItem>\n                  ))}\n                </DropdownMenuContent>\n              </DropdownMenu>\n            )}\n          </div>\n        </div>\n      )}\n\n      <div\n        className={styles.container({ className: classNames?.container })}\n        style={maxHeight ? { maxHeight } : undefined}\n        // Lets keyboard users scroll a table wider or taller than its box.\n        tabIndex={0}\n        // Only a named landmark when there's a caption; unnamed duplicates would clutter the landmark list.\n        role={caption ? \"region\" : undefined}\n        aria-label={caption}\n      >\n        <table className={styles.table({ className: classNames?.table })} aria-busy={isLoading || undefined}>\n          {caption && <caption className=\"sr-only\">{caption}</caption>}\n          <thead className={styles.thead({ className: classNames?.thead })}>\n            {table.getHeaderGroups().map((group) => (\n              <tr key={group.id}>\n                {group.headers.map((header) => {\n                  const canSort = header.column.getCanSort();\n                  const dir = header.column.getIsSorted();\n                  const meta = header.column.columnDef.meta as { className?: string; align?: \"left\" | \"right\" | \"center\" } | undefined;\n                  return (\n                    <th\n                      key={header.id}\n                      scope=\"col\"\n                      colSpan={header.colSpan}\n                      aria-sort={canSort ? (dir === \"asc\" ? \"ascending\" : dir === \"desc\" ? \"descending\" : \"none\") : undefined}\n                      className={styles.headerCell({\n                        className: cn(meta?.className, meta?.align === \"right\" && \"text-right\", classNames?.headerCell),\n                      })}\n                    >\n                      {header.isPlaceholder ? null : canSort ? (\n                        <button\n                          type=\"button\"\n                          onClick={header.column.getToggleSortingHandler()}\n                          className={cn(\n                            \"-mx-1.5 inline-flex h-7 items-center gap-1 rounded-xs px-1.5 transition-colors duration-fast ease-standard\",\n                            \"hover:bg-accent hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring\",\n                            dir && \"text-foreground\",\n                            meta?.align === \"right\" && \"flex-row-reverse\",\n                          )}\n                        >\n                          {flexRender(header.column.columnDef.header, header.getContext())}\n                          <SortIcon dir={dir} />\n                        </button>\n                      ) : (\n                        flexRender(header.column.columnDef.header, header.getContext())\n                      )}\n                    </th>\n                  );\n                })}\n              </tr>\n            ))}\n          </thead>\n          <tbody>\n            {isLoading ? (\n              Array.from({ length: skeletonCount }, (_, i) => (\n                <tr key={`skeleton-${i}`} className={styles.row({ className: classNames?.row })}>\n                  {visibleColumns.map((column, j) => (\n                    <td key={column.id} className={styles.cell({ className: classNames?.cell })}>\n                      <Skeleton className={cn(\"h-4\", column.id === \"__select\" ? \"size-4\" : j % 3 === 0 ? \"w-3/4\" : \"w-1/2\")} />\n                    </td>\n                  ))}\n                </tr>\n              ))\n            ) : rows.length === 0 ? (\n              <tr>\n                <td colSpan={visibleColumns.length || 1} className=\"p-0\">\n                  {emptyState ?? (\n                    <EmptyState\n                      size=\"sm\"\n                      icon={<InboxIcon />}\n                      title={globalFilter ? \"No matching results\" : \"No results\"}\n                      description={globalFilter ? `Nothing matches “${globalFilter}”. Try a different search.` : undefined}\n                    />\n                  )}\n                </td>\n              </tr>\n            ) : (\n              rows.map((row) => (\n                <tr\n                  key={row.id}\n                  data-state={row.getIsSelected() ? \"selected\" : undefined}\n                  aria-selected={enableRowSelection ? row.getIsSelected() : undefined}\n                  className={styles.row({ className: classNames?.row })}\n                >\n                  {row.getVisibleCells().map((cell) => {\n                    const meta = cell.column.columnDef.meta as { className?: string; align?: \"left\" | \"right\" | \"center\" } | undefined;\n                    return (\n                      <td\n                        key={cell.id}\n                        className={styles.cell({\n                          className: cn(meta?.className, meta?.align === \"right\" && \"text-right tabular-nums\", classNames?.cell),\n                        })}\n                      >\n                        {flexRender(cell.column.columnDef.cell, cell.getContext())}\n                      </td>\n                    );\n                  })}\n                </tr>\n              ))\n            )}\n          </tbody>\n        </table>\n      </div>\n\n      {paged && (\n        <div className={styles.footer({ className: classNames?.footer })}>\n          <p aria-live=\"polite\" className=\"tabular-nums\">\n            {enableRowSelection && selectedCount > 0\n              ? `${selectedCount} of ${filteredCount} selected`\n              : `${filteredCount} ${filteredCount === 1 ? \"row\" : \"rows\"}`}\n          </p>\n          <div className=\"flex flex-wrap items-center gap-3\">\n            {sizeOptions.length > 1 && (\n              <div className=\"flex items-center gap-2\">\n                <span id={pageSizeLabelId} className=\"whitespace-nowrap\">\n                  Rows per page\n                </span>\n                <Select\n                  value={String(pagination.pageSize)}\n                  onValueChange={(v) => setPagination({ pageIndex: 0, pageSize: Number(v) })}\n                >\n                  <SelectTrigger size=\"sm\" className=\"w-18\" aria-labelledby={pageSizeLabelId}>\n                    <SelectValue />\n                  </SelectTrigger>\n                  <SelectContent>\n                    {sizeOptions.map((n) => (\n                      <SelectItem key={n} value={String(n)}>\n                        {n}\n                      </SelectItem>\n                    ))}\n                  </SelectContent>\n                </Select>\n              </div>\n            )}\n            <Pagination\n              size=\"sm\"\n              page={pagination.pageIndex + 1}\n              pageCount={totalPages}\n              onPageChange={(p) => setPagination({ ...pagination, pageIndex: p - 1 })}\n              label={caption ? `${caption} pages` : \"Table pages\"}\n            />\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n"
    }
  ],
  "docs": "Docs and examples: https://junco.madebytobi.com/docs/components/data-table",
  "meta": {
    "hash": "052065ca1ff11d32",
    "source": "packages/react/src/data-table/data-table.tsx",
    "exports": [
      "dataTableVariants",
      "DataTableProps",
      "DataTable"
    ],
    "props": {
      "DataTable": {
        "extends": "<TData>",
        "props": [
          {
            "name": "columns",
            "optional": false,
            "type": "ColumnDef<TData"
          },
          {
            "name": "data",
            "optional": false,
            "type": "TData[]"
          },
          {
            "name": "getRowId",
            "optional": true,
            "type": "(row: TData, index: number) => string",
            "description": "Stable row ids (recommended with selection). Defaults to the row index."
          },
          {
            "name": "isLoading",
            "optional": true,
            "type": "boolean",
            "description": "Shows skeleton rows and sets aria-busy."
          },
          {
            "name": "loadingRows",
            "optional": true,
            "type": "number",
            "description": "Skeleton rows while loading. Defaults to the page size (max 10)."
          },
          {
            "name": "enableRowSelection",
            "optional": true,
            "type": "boolean | ((row: TData) => boolean)",
            "description": "Adds a checkbox column."
          },
          {
            "name": "rowSelection",
            "optional": true,
            "type": "RowSelectionState"
          },
          {
            "name": "onRowSelectionChange",
            "optional": true,
            "type": "(selection: RowSelectionState) => void"
          },
          {
            "name": "enableGlobalFilter",
            "optional": true,
            "type": "boolean",
            "description": "Shows a search box that filters across all columns."
          },
          {
            "name": "globalFilter",
            "optional": true,
            "type": "string"
          },
          {
            "name": "onGlobalFilterChange",
            "optional": true,
            "type": "(value: string) => void"
          },
          {
            "name": "searchPlaceholder",
            "optional": true,
            "type": "string"
          },
          {
            "name": "enableColumnVisibility",
            "optional": true,
            "type": "boolean",
            "description": "Shows a \"Columns\" menu to hide and show columns."
          },
          {
            "name": "columnVisibility",
            "optional": true,
            "type": "VisibilityState"
          },
          {
            "name": "onColumnVisibilityChange",
            "optional": true,
            "type": "(visibility: VisibilityState) => void"
          },
          {
            "name": "sorting",
            "optional": true,
            "type": "SortingState"
          },
          {
            "name": "onSortingChange",
            "optional": true,
            "type": "(sorting: SortingState) => void"
          },
          {
            "name": "defaultSorting",
            "optional": true,
            "type": "SortingState"
          },
          {
            "name": "pageSize",
            "optional": true,
            "type": "number | false",
            "description": "Rows per page. Set `false` to show every row. Default 10."
          },
          {
            "name": "pageSizeOptions",
            "optional": true,
            "type": "number[]"
          },
          {
            "name": "pagination",
            "optional": true,
            "type": "PaginationState"
          },
          {
            "name": "onPaginationChange",
            "optional": true,
            "type": "(pagination: PaginationState) => void"
          },
          {
            "name": "manual",
            "optional": true,
            "type": "boolean",
            "description": "Server-side data: sorting, filtering and paging happen outside; pass the total page count."
          },
          {
            "name": "pageCount",
            "optional": true,
            "type": "number"
          },
          {
            "name": "rowCount",
            "optional": true,
            "type": "number",
            "description": "Total rows on the server, for the footer when `manual`."
          },
          {
            "name": "emptyState",
            "optional": true,
            "type": "React.ReactNode",
            "description": "Shown when there are no rows."
          },
          {
            "name": "toolbar",
            "optional": true,
            "type": "React.ReactNode",
            "description": "Extra controls on the right of the toolbar (filters, buttons)."
          },
          {
            "name": "caption",
            "optional": true,
            "type": "string",
            "description": "Accessible table name, also shown visually hidden as the caption."
          },
          {
            "name": "maxHeight",
            "optional": true,
            "type": "string",
            "description": "Max height of the scroll area, e.g. \"24rem\". The header stays visible while scrolling."
          },
          {
            "name": "density",
            "optional": true,
            "type": "\"compact\" | \"normal\" | \"relaxed\""
          },
          {
            "name": "isStickyHeader",
            "optional": true,
            "type": "boolean"
          },
          {
            "name": "tableOptions",
            "optional": true,
            "type": "Partial<TableOptions<TData>>",
            "description": "Escape hatch for anything else TanStack Table supports."
          },
          {
            "name": "onTableReady",
            "optional": true,
            "type": "(table: TanstackTable<TData>) => void",
            "description": "Receives the table instance (for custom toolbars or bulk actions)."
          },
          {
            "name": "className",
            "optional": true,
            "type": "string"
          },
          {
            "name": "classNames",
            "optional": true,
            "type": "Partial<Record<Slots"
          }
        ]
      }
    },
    "variants": {
      "variants": {
        "density": [
          "compact",
          "normal",
          "relaxed"
        ],
        "isStickyHeader": [
          "true"
        ]
      },
      "defaultVariants": {
        "density": "normal",
        "isStickyHeader": "true"
      }
    },
    "usage": "import { DataTable, type ColumnDef } from \"junco/data-table\";",
    "docsUrl": "https://junco.madebytobi.com/docs/components/data-table"
  }
}
