{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "title": "Data table",
  "author": "Tori Bryan",
  "description": "A table for lists people work through, on TanStack Table: sorting, search, filters with counts, pages, selection, cell types, locked rows and pinned columns.",
  "dependencies": [
    "lucide-react",
    "@tanstack/react-table",
    "@tanstack/react-store"
  ],
  "registryDependencies": [
    "https://fibo.toribryan.com/r/avatar.json",
    "https://fibo.toribryan.com/r/button.json",
    "https://fibo.toribryan.com/r/checkbox.json",
    "https://fibo.toribryan.com/r/count.json",
    "https://fibo.toribryan.com/r/empty-state.json",
    "https://fibo.toribryan.com/r/separator.json",
    "https://fibo.toribryan.com/r/skeleton.json",
    "https://fibo.toribryan.com/r/input-group.json",
    "https://fibo.toribryan.com/r/menu.json",
    "https://fibo.toribryan.com/r/pagination.json",
    "https://fibo.toribryan.com/r/table.json",
    "https://fibo.toribryan.com/r/tooltip.json",
    "https://fibo.toribryan.com/r/sheet.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/data-table.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  ArrowDownIcon,\n  ArrowUpIcon,\n  ChevronsUpDownIcon,\n  Columns3Icon,\n  EllipsisIcon,\n  ListFilterIcon,\n  LockIcon,\n  SearchIcon,\n  SlidersHorizontalIcon,\n  Trash2Icon,\n  XIcon,\n} from \"lucide-react\"\nimport {\n  columnFacetingFeature,\n  columnFilteringFeature,\n  columnPinningFeature,\n  columnVisibilityFeature,\n  constructFilterFn,\n  createFacetedRowModel,\n  createFacetedUniqueValues,\n  createFilteredRowModel,\n  createPaginatedRowModel,\n  createSortedRowModel,\n  createTableHook,\n  createTableHookContexts,\n  filterFn_arrHas,\n  filterFn_equals,\n  filterFn_includesString,\n  FlexRender,\n  functionalUpdate,\n  globalFilteringFeature,\n  makeStateUpdater,\n  metaHelper,\n  rowPaginationFeature,\n  rowSelectionFeature,\n  rowSortingFeature,\n  sortFn_alphanumeric,\n  sortFn_basic,\n  sortFn_datetime,\n  sortFn_text,\n  tableFeatures,\n  type Column,\n  type ColumnFiltersState,\n  type Header,\n  type PaginationState,\n  type ReactTable,\n  type Row,\n  type RowData,\n  type SortingState,\n  type TableOptions,\n  type TableState,\n  type Updater,\n} from \"@tanstack/react-table\"\nimport {\n  createAtom,\n  shallow,\n  useSelector,\n  type Atom,\n} from \"@tanstack/react-store\"\n\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarImage,\n} from \"@/components/ui/avatar\"\nimport { Button } from \"@/components/ui/button\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport { Count } from \"@/components/ui/count\"\nimport {\n  EmptyState,\n  EmptyStateActions,\n  EmptyStateDescription,\n  EmptyStateTitle,\n} from \"@/components/ui/empty-state\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupInput,\n} from \"@/components/ui/input-group\"\nimport {\n  Menu,\n  MenuCheckboxItem,\n  MenuContent,\n  MenuGroup,\n  MenuLabel,\n  MenuTrigger,\n} from \"@/components/ui/menu\"\nimport {\n  Pagination,\n  type PaginationProps,\n} from \"@/components/ui/pagination\"\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport {\n  Sheet,\n  SheetBody,\n  SheetContent,\n  SheetHeader,\n  SheetTitle,\n  SheetTrigger,\n} from \"@/components/ui/sheet\"\nimport { mergeRefs } from \"@/lib/merge-refs\"\nimport { cn } from \"@/lib/utils\"\n\ntype DataTableNoun = { one: string; other: string }\n\n/** What a column holds, which sets its alignment and what the cell renders. */\ntype DataTableCellType =\n  \"primary\" | \"text\" | \"person\" | \"status\" | \"numeric\" | \"actions\"\n\n/** What a narrow table shows: the table, scrolling sideways, or a card per row. */\ntype DataTableNarrowLayout = \"scroll\" | \"cards\"\n\n/** Which edge a column sticks to while the table scrolls sideways. */\ntype DataTablePinned = \"none\" | \"start\" | \"end\"\n\ntype DataTableAvatar = { src?: string; fallback: string }\n\n/*\n * Method signatures on purpose: their parameters are checked both ways, so a\n * column can write `avatar: (member: Member) => ...` although the features\n * object, which types meta, can't know the row type.\n */\n/** What a column tells Data table about itself, through `meta`. */\ntype DataTableColumnMeta = {\n  /** What the column holds. Sets its alignment and how its cells render. */\n  type?: DataTableCellType\n  /** Its name in the Columns menu, filters and cards, when the header isn't text. */\n  label?: string\n  /** Classes for the column's head and cells, such as a width. */\n  className?: string\n  /** A second, smaller line under the value. Makes the row 52px tall. */\n  secondary?(row: unknown): React.ReactNode\n  /** The person's photo on a person column. Initials show while it loads. */\n  avatar?(row: unknown): DataTableAvatar | undefined\n  /** An icon before the value, on a primary column. */\n  icon?: React.ReactNode\n}\n\ntype DataTableTableMeta = {\n  /** Why a row can't be selected. useDataTable sets it from `lockedReason`. */\n  lockedReason?(row: unknown): React.ReactNode\n}\n\n/*\n * Only what Data table uses, so a table carries nothing else. Each row-model\n * slot comes after the feature it needs, and the registries hold just what\n * the cell types call for: words, numbers and dates to sort by, and a search,\n * one value or a list of values to filter on.\n */\nconst dataTableFeatures = tableFeatures({\n  rowSortingFeature,\n  sortedRowModel: createSortedRowModel(),\n  sortFns: {\n    text: sortFn_text,\n    // Sorting on \"auto\" picks this for words with digits in them.\n    alphanumeric: sortFn_alphanumeric,\n    basic: sortFn_basic,\n    datetime: sortFn_datetime,\n  },\n  columnFilteringFeature,\n  globalFilteringFeature,\n  filteredRowModel: createFilteredRowModel(),\n  filterFns: {\n    includesString: filterFn_includesString,\n    equals: filterFn_equals,\n    // A lone value, as a hand-written link might carry, reads as a list of\n    // one; arrHas would otherwise test it letter by letter and match nothing.\n    arrHas: constructFilterFn({\n      ...filterFn_arrHas,\n      resolveFilterValue: (value: unknown) =>\n        Array.isArray(value) ? value : [value],\n    }),\n  },\n  columnFacetingFeature,\n  facetedRowModel: createFacetedRowModel(),\n  facetedUniqueValues: createFacetedUniqueValues(),\n  rowPaginationFeature,\n  paginatedRowModel: createPaginatedRowModel(),\n  rowSelectionFeature,\n  columnVisibilityFeature,\n  columnPinningFeature,\n  columnMeta: metaHelper<DataTableColumnMeta>(),\n  tableMeta: metaHelper<DataTableTableMeta>(),\n})\n\ntype DataTableFeatures = typeof dataTableFeatures\n\n/**\n * A table made by useDataTable, for `<DataTable table={table}>`. Whatever\n * the owner selected into `state` stays its own business.\n */\ntype DataTableInstance<TData extends RowData = RowData> = Omit<\n  ReactTable<DataTableFeatures, TData>,\n  \"state\"\n>\n\ntype DataTableAnyTable = DataTableInstance<RowData>\ntype DataTableAnyRow = Row<DataTableFeatures, RowData>\ntype DataTableAnyColumn = Column<DataTableFeatures, RowData, unknown>\n\n// Scoped contexts, so a Data table inside an app's own TanStack table\n// never answers to the outer one's hooks.\nconst dataTableContexts = createTableHookContexts<DataTableFeatures>()\n\nconst dataTableHook = createTableHook({\n  features: dataTableFeatures,\n  tableContext: dataTableContexts.tableContext,\n  cellContext: dataTableContexts.cellContext,\n  headerContext: dataTableContexts.headerContext,\n  // Selection is kept by id, so it survives sorting, filtering and paging.\n  getRowId: (row: { id?: unknown }) => {\n    if (typeof row.id === \"string\" || typeof row.id === \"number\") {\n      return String(row.id)\n    }\n    throw new Error(\n      \"Data table rows need an id, or pass getRowId to useDataTable.\"\n    )\n  },\n  // A checkbox's change event drops Shift, so rows pass it along themselves.\n  isRowRangeSelectionEvent: (event) =>\n    (event as { shiftKey?: boolean }).shiftKey === true,\n})\n\n/** A column helper bound to Data table's features, so `meta` and fns are typed. */\nconst createDataTableColumnHelper = dataTableHook.createAppColumnHelper\n\n/** The table of the Data table this is inside, for parts of your own. */\nconst useDataTableContext = dataTableHook.useTableContext\n\ntype DataTableOptions<TData extends RowData> = Omit<\n  TableOptions<DataTableFeatures, TData>,\n  \"features\"\n> & {\n  /** Why a row can't be selected. Return nothing for a row that can be. */\n  lockedReason?: (row: TData) => React.ReactNode\n}\n\n// Every row shows until a table asks for pages, so leaving out the footer\n// never hides rows.\nconst EVERY_ROW: PaginationState = { pageIndex: 0, pageSize: Infinity }\n\nconst selectNothing = () => null\n\nconst hasReason = (reason: React.ReactNode) =>\n  reason !== undefined && reason !== null && reason !== false\n\n/**\n * Makes a Data table: its rows, columns and state, on Data table's features.\n * Without a selector, the component that calls it doesn't re-render on state\n * changes; the parts subscribe to what each shows.\n */\ntype PageResetKey = \"sorting\" | \"columnFilters\" | \"globalFilter\"\n\n/*\n * Wraps a change handler so a change that lands also sends the page back to\n * the first, the way TanStack's autoResetPageIndex does, but only for changes\n * made through the table, not for an atom restored from the URL.\n */\nfunction resetPageOnChange<T>(\n  key: PageResetKey,\n  getTable: () => DataTableAnyTable | null,\n  onChange: ((updater: Updater<T>) => void) | undefined\n) {\n  return (updater: Updater<T>) => {\n    const table = getTable()\n    if (!table) return onChange?.(updater)\n    const before = table.atoms[key].get() as T\n    const changed = !Object.is(functionalUpdate(updater, before), before)\n    if (onChange) onChange(updater)\n    else makeStateUpdater(key, table)(updater as never)\n    if (changed && table.atoms.pagination.get().pageIndex !== 0) {\n      table.resetPageIndex(true)\n    }\n  }\n}\n\n/**\n * Makes a Data table: its rows, columns and state, on Data table's features.\n * Without a selector, the component that calls it doesn't re-render on state\n * changes; the parts subscribe to what each shows.\n */\nfunction useDataTable<TData extends RowData, TSelected = null>(\n  {\n    lockedReason,\n    initialState,\n    meta,\n    enableRowSelection,\n    autoResetPageIndex,\n    onSortingChange,\n    onColumnFiltersChange,\n    onGlobalFilterChange,\n    ...options\n  }: DataTableOptions<TData>,\n  selector?: (state: TableState<DataTableFeatures>) => TSelected\n) {\n  /*\n   * TanStack's autoResetPageIndex also fires when `data` changes, so rows\n   * arriving from a fetch would send a page restored from a link back to the\n   * first. Unless the table sets it, the page resets when someone sorts,\n   * filters or searches instead, and data that leaves it past the last page\n   * moves it to the last.\n   */\n  const ownReset =\n    autoResetPageIndex === undefined &&\n    options.autoResetAll === undefined &&\n    !options.manualPagination\n  // The change handlers go in before the table exists; they run later, from\n  // events, and find it here.\n  const [built] = React.useState(() =>\n    createAtom<DataTableAnyTable | null>(null)\n  )\n  const getTable = () => built.get()\n\n  const table = dataTableHook.useAppTable<TData, TSelected>(\n    {\n      ...options,\n      autoResetPageIndex: ownReset ? false : autoResetPageIndex,\n      ...(ownReset\n        ? {\n            onSortingChange: resetPageOnChange(\n              \"sorting\",\n              getTable,\n              onSortingChange\n            ),\n            onColumnFiltersChange: resetPageOnChange(\n              \"columnFilters\",\n              getTable,\n              onColumnFiltersChange\n            ),\n            onGlobalFilterChange: resetPageOnChange(\n              \"globalFilter\",\n              getTable,\n              onGlobalFilterChange\n            ),\n          }\n        : // Left out when unset, so TanStack's own handlers stay in place.\n          Object.fromEntries(\n            Object.entries({\n              onSortingChange,\n              onColumnFiltersChange,\n              onGlobalFilterChange,\n            }).filter(([, handler]) => handler)\n          )),\n      initialState: { pagination: EVERY_ROW, ...initialState },\n      meta: { ...meta, lockedReason },\n      enableRowSelection:\n        enableRowSelection ??\n        (lockedReason ? (row) => !hasReason(lockedReason(row.original)) : true),\n    },\n    selector ??\n      (selectNothing as unknown as (\n        state: TableState<DataTableFeatures>\n      ) => TSelected)\n  )\n\n  React.useLayoutEffect(() => {\n    built.set(() => table as unknown as DataTableAnyTable)\n  })\n\n  const { data } = options\n  React.useEffect(() => {\n    if (!ownReset || !table.getPrePaginatedRowModel().rows.length) return\n    const last = table.getPageCount() - 1\n    if (table.atoms.pagination.get().pageIndex > last) table.setPageIndex(last)\n  }, [table, data, ownReset])\n\n  return table\n}\n\n/** How an atom's value is written to and read from a URL search param. */\ntype SearchParamCodec<T> = {\n  /** Reads the value from the param's text, or from null when it's absent. */\n  parse: (text: string | null) => T\n  /** Writes the value as text, or null to leave the param out. */\n  serialize: (value: T) => string | null\n}\n\nfunction readSearchParam<T>(key: string, codec: SearchParamCodec<T>) {\n  return codec.parse(new URLSearchParams(window.location.search).get(key))\n}\n\n/**\n * An atom kept in the URL's `key` param, for a table's sorting, filters or\n * page. It writes back with history.replaceState, so the back button isn't\n * filled with every keystroke, and follows back and forward. It starts from\n * the codec's empty value on the server and on the first client render, so\n * hydration matches; the URL's value applies after mount, before paint.\n */\nfunction useSearchParamsAtom<T>(\n  key: string,\n  codec: SearchParamCodec<T>\n): Atom<T> {\n  // The empty value at first, on the server and the client alike, so\n  // hydration matches; the URL's value lands once mounted.\n  const [atom] = React.useState(() => createAtom(codec.parse(null)))\n  // Codecs are often written inline; the latest one is used without\n  // resubscribing.\n  const codecRef = React.useRef(codec)\n  React.useEffect(() => {\n    codecRef.current = codec\n  })\n\n  React.useLayoutEffect(() => {\n    // Read before the writer subscribes, so restoring from the URL doesn't\n    // write it back.\n    atom.set(readSearchParam(key, codecRef.current))\n    const write = (value: T) => {\n      const params = new URLSearchParams(window.location.search)\n      const text = codecRef.current.serialize(value)\n      if (params.get(key) === text) return\n      if (text === null) params.delete(key)\n      else params.set(key, text)\n      const search = params.toString()\n      window.history.replaceState(\n        window.history.state,\n        \"\",\n        `${window.location.pathname}${search ? `?${search}` : \"\"}${window.location.hash}`\n      )\n    }\n    const onPopState = () => atom.set(readSearchParam(key, codecRef.current))\n    const subscription = atom.subscribe(write)\n    window.addEventListener(\"popstate\", onPopState)\n    return () => {\n      subscription.unsubscribe()\n      window.removeEventListener(\"popstate\", onPopState)\n    }\n  }, [atom, key])\n\n  return atom\n}\n\ntype FilterScalar = string | number | boolean\n\nconst isFilterScalar = (value: unknown): value is FilterScalar =>\n  typeof value === \"string\" ||\n  typeof value === \"boolean\" ||\n  (typeof value === \"number\" && Number.isFinite(value))\n\n/*\n * A link can carry anything, and TanStack reads a filter's id and hands its\n * value straight to the filter function, so `[null]` would throw there. Only\n * what Data table's filters take gets through: a column id, and a word,\n * number or flag, or a list of them, once a column.\n */\nfunction isColumnFilter(entry: unknown): entry is ColumnFiltersState[number] {\n  if (typeof entry !== \"object\" || entry === null) return false\n  const { id, value } = entry as { id?: unknown; value?: unknown }\n  if (typeof id !== \"string\" || id === \"\") return false\n  return Array.isArray(value)\n    ? value.length > 0 && value.every(isFilterScalar)\n    : isFilterScalar(value)\n}\n\nconst SORT_DIRECTIONS: Record<string, boolean> = { asc: false, desc: true }\n\n/** Codecs for useSearchParamsAtom, one per slice a link usually carries. */\nconst dataTableCodecs = {\n  /** Text, such as the search. Empty leaves the param out. */\n  text: {\n    parse: (text) => text ?? \"\",\n    serialize: (value) => value || null,\n  } satisfies SearchParamCodec<string>,\n  /**\n   * Sorting as `column.direction` pairs: `name.asc,projects.desc`. A pair\n   * with no direction sorts up; empty ids and repeats of a column are left\n   * out.\n   */\n  sorting: {\n    parse: (text) => {\n      const sorting: SortingState = []\n      for (const pair of (text ?? \"\").split(\",\")) {\n        const at = pair.lastIndexOf(\".\")\n        const direction = at === -1 ? undefined : pair.slice(at + 1)\n        const desc =\n          direction !== undefined && Object.hasOwn(SORT_DIRECTIONS, direction)\n            ? SORT_DIRECTIONS[direction]\n            : undefined\n        const id = desc === undefined ? pair : pair.slice(0, at)\n        if (!id || sorting.some((sort) => sort.id === id)) continue\n        sorting.push({ id, desc: desc ?? false })\n      }\n      return sorting\n    },\n    serialize: (value) =>\n      value.map(({ id, desc }) => `${id}.${desc ? \"desc\" : \"asc\"}`).join(\",\") ||\n      null,\n  } satisfies SearchParamCodec<SortingState>,\n  /** Column filters as JSON, so any filter value survives the trip. */\n  columnFilters: {\n    parse: (text) => {\n      if (!text) return []\n      try {\n        const value: unknown = JSON.parse(text)\n        if (!Array.isArray(value)) return []\n        // One filter a column: TanStack would apply the last and the facet\n        // menu would show the first.\n        return value\n          .filter(isColumnFilter)\n          .filter(\n            (filter, index, all) =>\n              all.findIndex(({ id }) => id === filter.id) === index\n          )\n      } catch {\n        return []\n      }\n    },\n    serialize: (value) => (value.length ? JSON.stringify(value) : null),\n  } satisfies SearchParamCodec<ColumnFiltersState>,\n  /**\n   * The page, counting from 1, at a fixed page size. Page 1 leaves it out,\n   * and so does anything but a whole number a page index can hold.\n   */\n  pagination: (pageSize: number): SearchParamCodec<PaginationState> => {\n    // A size the table can't page by shows every row rather than none.\n    const size = pageSize > 0 ? pageSize : EVERY_ROW.pageSize\n    return {\n      parse: (text) => {\n        const page = /^[1-9]\\d*$/.test(text ?? \"\") ? Number(text) : 0\n        return {\n          pageIndex: Number.isSafeInteger(page) && page > 1 ? page - 1 : 0,\n          pageSize: size,\n        }\n      },\n      serialize: (value) =>\n        Number.isSafeInteger(value.pageIndex) && value.pageIndex > 0\n          ? String(value.pageIndex + 1)\n          : null,\n    }\n  },\n}\n\ntype DataTableContextValue = {\n  table: DataTableAnyTable\n  noun: DataTableNoun\n  clear: () => void\n  selectAllMatching: () => void\n  hasSelection: () => boolean\n  showSelectedOnly: boolean\n  onShowSelectedOnlyChange?: (showSelectedOnly: boolean) => void\n  narrow: boolean\n  cards: boolean\n  narrowLayout: DataTableNarrowLayout\n  empty: React.ReactNode\n  loading: boolean\n}\n\nconst DataTableContext = React.createContext<DataTableContextValue | null>(null)\n\nfunction useDataTableRoot() {\n  const context = React.useContext(DataTableContext)\n  if (!context) {\n    throw new Error(\"Data table parts must be inside a DataTable.\")\n  }\n  return context\n}\n\ntype DataTableRowContextValue = {\n  id: string\n  labelId: string\n  reasonId: string\n  lockedReason?: React.ReactNode\n}\n\nconst DataTableRowContext =\n  React.createContext<DataTableRowContextValue | null>(null)\n\nconst DEFAULT_NOUN: DataTableNoun = { one: \"row\", other: \"rows\" }\n\nfunction countLabel(count: number, noun: DataTableNoun) {\n  return `${count.toLocaleString(\"en-US\")} ${count === 1 ? noun.one : noun.other}`\n}\n\ntype SelectionSummary = {\n  count: number\n  /** Selected rows the filters hide, which the count includes. */\n  hiddenCount: number\n  totalCount: number\n  matchingCount: number\n  pageState: \"none\" | \"some\" | \"all\"\n  selectableOnPage: number\n  isAll: boolean\n  canSelectAllMatching: boolean\n}\n\n/*\n * The selected rows there are to act on: every selected row still in the\n * data, as getSelectedRowModel has them, whether or not a filter hides it.\n * Ids a refetch left behind would otherwise be counted, and deleted, though\n * the rows are gone. With manualPagination the data is one page, so every\n * id counts; the app owns the rest.\n */\nfunction selectedRowIds(table: DataTableAnyTable): string[] {\n  if (table.options.manualPagination) {\n    return Object.keys(table.atoms.rowSelection.get())\n  }\n  return table.getSelectedRowModel().flatRows.map((row) => row.id)\n}\n\n// Selected rows in the data that the filters leave out.\nfunction hiddenSelectedCount(table: DataTableAnyTable) {\n  return (\n    table.getSelectedRowModel().flatRows.length -\n    table.getFilteredSelectedRowModel().flatRows.length\n  )\n}\n\n// “5 members selected, 2 hidden by filters”: the toolbar and the announcer\n// say the same thing.\nfunction selectionLabel(summary: SelectionSummary, noun: DataTableNoun) {\n  if (summary.isAll && summary.hiddenCount === 0) {\n    return `All ${countLabel(summary.matchingCount, noun)} selected`\n  }\n  return `${countLabel(summary.count, noun)} selected${hiddenSuffix(summary.hiddenCount)}`\n}\n\nfunction hiddenSuffix(hiddenCount: number) {\n  return hiddenCount > 0\n    ? `, ${hiddenCount.toLocaleString(\"en-US\")} hidden by filters`\n    : \"\"\n}\n\nfunction summarize(table: DataTableAnyTable): SelectionSummary {\n  let selectableOnPage = 0\n  let selectedOnPage = 0\n  for (const row of table.getRowModel().rows) {\n    if (!row.getCanSelect()) continue\n    selectableOnPage++\n    if (row.getIsSelected()) selectedOnPage++\n  }\n  const pageState =\n    selectedOnPage === 0\n      ? \"none\"\n      : selectedOnPage === selectableOnPage\n        ? \"all\"\n        : \"some\"\n\n  // Loaded rows only: with manualPagination, the app selects across its\n  // other pages itself.\n  const matchingCount = table\n    .getFilteredRowModel()\n    .flatRows.filter((row) => row.getCanSelect()).length\n  const count = selectedRowIds(table).length\n  const hiddenCount = hiddenSelectedCount(table)\n  // Out of the rows that match plus the selected ones the filters hide, so\n  // the count never runs past the total.\n  const totalCount = table.getRowCount() + hiddenCount\n  const isAll =\n    matchingCount > selectableOnPage &&\n    count > 0 &&\n    table.getIsAllRowsSelected()\n  return {\n    count,\n    hiddenCount,\n    totalCount,\n    matchingCount,\n    pageState,\n    selectableOnPage,\n    isAll,\n    canSelectAllMatching:\n      !isAll && pageState === \"all\" && count - hiddenCount < matchingCount,\n  }\n}\n\n// Counts for the toolbar, select all and the footer. Shallow-compared, so a\n// part re-renders only when a number it shows changes.\nfunction useSelectionSummary() {\n  const { table } = useDataTableRoot()\n  return useSelector(table.store, () => summarize(table), {\n    compare: shallow,\n  })\n}\n\nfunction useRowSelected(id: string) {\n  const { table } = useDataTableRoot()\n  return useSelector(\n    table.atoms.rowSelection,\n    (selection) => selection[id] === true\n  )\n}\n\nfunction toggleRow(\n  table: DataTableAnyTable,\n  id: string,\n  checked: boolean,\n  extend: boolean\n) {\n  const row = table.getCoreRowModel().rowsById[id]\n  // Through the handler, so Shift+click selects the range from the last row\n  // toggled, in the order the rows are shown.\n  row?.getToggleSelectedHandler()({ target: { checked }, shiftKey: extend })\n}\n\nfunction shiftKeyOf(details: { event: Event }) {\n  return \"shiftKey\" in details.event && details.event.shiftKey === true\n}\n\nfunction pinnedOf(column: DataTableAnyColumn): DataTablePinned {\n  return column.getIsPinned() || \"none\"\n}\n\nfunction labelOf(column: DataTableAnyColumn) {\n  const header = column.columnDef.header\n  return (\n    column.columnDef.meta?.label ??\n    (typeof header === \"string\" ? header : column.id)\n  )\n}\n\ntype DataTableProps<TData extends RowData = RowData> =\n  React.ComponentProps<\"div\"> & {\n    /** The table from useDataTable: its rows, columns and state. */\n    table: DataTableInstance<TData>\n    /** What a row is, for counts and labels: { one: \"member\", other: \"members\" }. */\n    noun?: DataTableNoun\n    /** Whether the app is showing only the selected rows. */\n    showSelectedOnly?: boolean\n    /** Shows “Show selected only” while rows are selected; the app filters its rows. */\n    onShowSelectedOnlyChange?: (showSelectedOnly: boolean) => void\n    /** Under 32rem: keep the table and scroll it, or show DataTableCards instead. */\n    narrowLayout?: DataTableNarrowLayout\n    /** Shown in place of the rows when there are none. Defaults to DataTableEmpty. */\n    empty?: React.ReactNode\n    /** Shows skeleton rows in place of the body while the data loads. */\n    loading?: boolean\n  }\n\n/**\n * The root. Give it `table` from useDataTable; it renders the rows, and its\n * parts read the table's state.\n */\nfunction DataTable<TData extends RowData>({\n  table: tableProp,\n  showSelectedOnly = false,\n  onShowSelectedOnlyChange,\n  ...props\n}: DataTableProps<TData>) {\n  const table = tableProp as unknown as DataTableAnyTable\n  const clear = React.useCallback(() => table.resetRowSelection(true), [table])\n  const selectAllMatching = React.useCallback(\n    () => table.toggleAllRowsSelected(true),\n    [table]\n  )\n  const hasSelection = React.useCallback(\n    () => selectedRowIds(table).length > 0,\n    [table]\n  )\n  const selecting = useSelector(\n    table.store,\n    () => selectedRowIds(table).length > 0\n  )\n\n  // Drop ids a refetch left behind, so table.getSelectedRowIds(), which bulk\n  // actions read, agrees with the toolbar, the footer and the announcer.\n  // Rows a filter hides stay selected, and the toolbar says how many.\n  const stale = useSelector(table.store, () => {\n    const selection = Object.keys(table.atoms.rowSelection.get())\n    const kept = selectedRowIds(table)\n    return selection.length !== kept.length\n  })\n  React.useEffect(() => {\n    if (!stale) return\n    const kept = new Set(selectedRowIds(table))\n    table.setRowSelection((old) =>\n      Object.fromEntries(Object.entries(old).filter(([id]) => kept.has(id)))\n    )\n  }, [stale, table])\n\n  // The app filters its own rows for Show selected only, so once nothing is\n  // selected there's nothing left to show.\n  React.useEffect(() => {\n    if (!selecting && showSelectedOnly) onShowSelectedOnlyChange?.(false)\n  }, [selecting, showSelectedOnly, onShowSelectedOnlyChange])\n\n  return (\n    <DataTableRoot\n      table={table}\n      clear={clear}\n      selectAllMatching={selectAllMatching}\n      hasSelection={hasSelection}\n      showSelectedOnly={showSelectedOnly}\n      onShowSelectedOnlyChange={onShowSelectedOnlyChange}\n      {...props}\n    />\n  )\n}\n\ntype RootProps = Omit<DataTableProps, \"table\">\n\nfunction DataTableRoot({\n  table,\n  clear,\n  selectAllMatching,\n  hasSelection,\n  className,\n  noun: nounProp = DEFAULT_NOUN,\n  showSelectedOnly = false,\n  onShowSelectedOnlyChange,\n  narrowLayout = \"scroll\",\n  empty,\n  loading = false,\n  children,\n  ref,\n  ...props\n}: RootProps &\n  Pick<\n    DataTableContextValue,\n    \"table\" | \"clear\" | \"selectAllMatching\" | \"hasSelection\"\n  >) {\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  // A caller's ref would otherwise replace the one Escape and the layout use.\n  const mergedRootRef = React.useMemo(() => mergeRefs(rootRef, ref), [ref])\n  // Keyed on the words, so a noun written inline keeps the context below\n  // stable from one render to the next.\n  const { one, other } = nounProp\n  const noun = React.useMemo(() => ({ one, other }), [one, other])\n\n  // Re-render on what can take a focused control away: the toolbar swapping,\n  // Select all matching going, rows leaving the page. The parts subscribe to\n  // the rest themselves, so this doesn't re-render them.\n  useSelector(\n    table.store,\n    () => {\n      const summary = summarize(table)\n      return {\n        selecting: summary.count > 0,\n        isAll: summary.isAll,\n        canSelectAllMatching: summary.canSelectAllMatching,\n        rows: table.getRowModel().rows,\n      }\n    },\n    { compare: shallow }\n  )\n\n  // A native listener, not onKeyDown: React events bubble out of portals,\n  // so Escape in an open row menu would otherwise clear the selection too.\n  React.useEffect(() => {\n    const root = rootRef.current\n    if (!root) return\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (event.key !== \"Escape\" || event.defaultPrevented) return\n      if (!hasSelection()) return\n      clear()\n    }\n    root.addEventListener(\"keydown\", onKeyDown)\n    return () => root.removeEventListener(\"keydown\", onKeyDown)\n  }, [hasSelection, clear])\n\n  // Narrow is the root's own width under 32rem, the same line the container\n  // queries for pinning use, so a table in a sidebar is narrow on a desktop.\n  const [narrow, setNarrow] = React.useState(false)\n  React.useLayoutEffect(() => {\n    const root = rootRef.current\n    if (!root) return\n    const update = () => {\n      const rem =\n        parseFloat(getComputedStyle(document.documentElement).fontSize) || 16\n      setNarrow(root.getBoundingClientRect().width < 32 * rem)\n    }\n    update()\n    const observer = new ResizeObserver(update)\n    observer.observe(root)\n    return () => observer.disconnect()\n  }, [])\n  const cards = narrow && narrowLayout === \"cards\"\n\n  // When the focused control disappears (Clear, Select all matching, a row\n  // hidden by Show selected only, a More menu whose trigger is gone), focus\n  // would fall to the page. Send it to Clear, or to select all when idle.\n  const lastFocusedRef = React.useRef<Element | null>(null)\n  // Set while focus is leaving, until it turns up again somewhere in the\n  // table, popups and menus it renders in portals included.\n  const leavingRef = React.useRef(false)\n  React.useEffect(() => {\n    const root = rootRef.current\n    if (!root) return\n    const onFocusIn = (event: FocusEvent) => {\n      lastFocusedRef.current = event.target as Element\n    }\n    root.addEventListener(\"focusin\", onFocusIn)\n    return () => root.removeEventListener(\"focusin\", onFocusIn)\n  }, [])\n  React.useLayoutEffect(() => {\n    if (!lastFocusedRef.current) return\n    const restore = () => {\n      const root = rootRef.current\n      const last = lastFocusedRef.current\n      if (!root || !last || last.isConnected) return\n      const active = document.activeElement\n      if (active && active !== document.body && active.isConnected) return\n      const target =\n        root.querySelector<HTMLElement>('[data-slot=\"data-table-clear\"]') ??\n        root.querySelector<HTMLElement>(\n          '[data-slot=\"data-table-select-all\"] [role=\"checkbox\"]'\n        )\n      lastFocusedRef.current = null\n      // The table and cards swapped: follow the row to its other form.\n      const rowId = last.closest(\"[data-row-id]\")?.getAttribute(\"data-row-id\")\n      const sameRow = rowId\n        ? root.querySelector<HTMLElement>(\n            `[data-row-id=\"${CSS.escape(rowId)}\"] [role=\"checkbox\"]`\n          )\n        : null\n      ;(sameRow ?? target)?.focus()\n    }\n    restore()\n    // A closing menu hands focus back on the next frame, to a trigger that\n    // may be gone by then.\n    const frame = requestAnimationFrame(restore)\n    return () => cancelAnimationFrame(frame)\n  })\n\n  // Once someone has left the table, a later refresh must not pull focus\n  // back into it. React's focus events, unlike the DOM's, bubble out of the\n  // portals the table's menus render in, so moving into one isn't leaving.\n  const onFocus = (event: React.FocusEvent<HTMLDivElement>) => {\n    leavingRef.current = false\n    props.onFocus?.(event)\n  }\n  const onBlur = (event: React.FocusEvent<HTMLDivElement>) => {\n    leavingRef.current = true\n    props.onBlur?.(event)\n    queueMicrotask(() => {\n      if (!leavingRef.current) return\n      const active = document.activeElement\n      if (active && active !== document.body) {\n        lastFocusedRef.current = null\n        return\n      }\n      // Focus fell to the page, which is also how a closing menu leaves it\n      // for a frame before handing it back; wait to see where it lands.\n      requestAnimationFrame(() =>\n        requestAnimationFrame(() => {\n          if (leavingRef.current) lastFocusedRef.current = null\n        })\n      )\n    })\n  }\n\n  const context = React.useMemo<DataTableContextValue>(\n    () => ({\n      table,\n      noun,\n      clear,\n      selectAllMatching,\n      hasSelection,\n      showSelectedOnly,\n      onShowSelectedOnlyChange,\n      narrow,\n      cards,\n      narrowLayout,\n      empty: empty === undefined ? <DataTableEmpty /> : empty,\n      loading,\n    }),\n    [\n      table,\n      noun,\n      clear,\n      selectAllMatching,\n      hasSelection,\n      showSelectedOnly,\n      onShowSelectedOnlyChange,\n      narrow,\n      cards,\n      narrowLayout,\n      empty,\n      loading,\n    ]\n  )\n\n  return (\n    <DataTableContext.Provider value={context}>\n      {/* The context is typed for any feature set; this one is Data table's. */}\n      <dataTableContexts.tableContext.Provider value={table as never}>\n        <div\n          ref={mergedRootRef}\n          data-slot=\"data-table\"\n          data-narrow={narrow || undefined}\n          data-layout={cards ? \"cards\" : \"table\"}\n          className={cn(\n            \"group/data-table @container/data-table flex w-full flex-col overflow-hidden rounded-lg border border-border bg-background\",\n            className\n          )}\n          {...props}\n          onFocus={onFocus}\n          onBlur={onBlur}\n        >\n          {children}\n          <DataTableAnnouncer />\n        </div>\n      </dataTableContexts.tableContext.Provider>\n    </DataTableContext.Provider>\n  )\n}\n\n/*\n * Announced from the selection that lands rather than the one asked for, so\n * a parent that turns a change down isn't contradicted.\n */\nfunction DataTableAnnouncer() {\n  const { table, noun } = useDataTableRoot()\n  const summary = useSelectionSummary()\n  const selection = useSelector(table.atoms.rowSelection)\n  const [announcement, setAnnouncement] = React.useState(\"\")\n  const previous = React.useRef(selection)\n\n  const announce = React.useEffectEvent(() => {\n    setAnnouncement(\n      summary.count === 0 ? \"Selection cleared\" : selectionLabel(summary, noun)\n    )\n  })\n\n  React.useEffect(() => {\n    const before = previous.current\n    previous.current = selection\n    if (before === selection) return\n    announce()\n  }, [selection])\n\n  return (\n    <span role=\"status\" className=\"sr-only\">\n      {announcement}\n    </span>\n  )\n}\n\n/*\n * One row for every action. A labeled group, not role=\"toolbar\": that\n * promises arrow keys between controls, and its search field needs them.\n * Idle, it holds filters and create actions;\n * while rows are selected it swaps, in place, to the selection and bulk\n * actions. Nothing else on the page acts on the selection.\n */\nfunction DataTableToolbar({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  const {\n    noun,\n    selectAllMatching,\n    clear,\n    showSelectedOnly,\n    onShowSelectedOnlyChange,\n    narrow,\n    cards,\n  } = useDataTableRoot()\n  const summary = useSelectionSummary()\n  const { count, matchingCount, canSelectAllMatching } = summary\n  const selecting = count > 0\n\n  return (\n    <div\n      role=\"group\"\n      aria-label={selecting ? \"Bulk actions\" : \"Filters and actions\"}\n      data-slot=\"data-table-toolbar\"\n      data-selecting={selecting || undefined}\n      className={cn(\n        \"flex min-h-12 items-center gap-2 border-b border-border px-3 py-2 data-selecting:gap-1 data-selecting:bg-muted data-selecting:pl-1\",\n        className\n      )}\n      {...props}\n    >\n      {cards ? (\n        // Cards have no header row, so select all moves up here.\n        <span data-slot=\"data-table-select-all\" className=\"flex px-2\">\n          <SelectAllCheckbox />\n        </span>\n      ) : null}\n      {selecting ? (\n        <>\n          <Button\n            data-slot=\"data-table-clear\"\n            variant=\"ghost\"\n            size=\"icon-sm\"\n            aria-label=\"Clear selection\"\n            onClick={clear}\n          >\n            <XIcon />\n          </Button>\n          <span\n            data-slot=\"data-table-selected-label\"\n            className=\"text-sm font-medium whitespace-nowrap tabular-nums\"\n          >\n            {selectionLabel(summary, noun)}\n          </span>\n          {canSelectAllMatching && !narrow ? (\n            <Button\n              data-slot=\"data-table-select-all-matching\"\n              variant=\"link\"\n              size=\"sm\"\n              onClick={selectAllMatching}\n            >\n              Select all {countLabel(matchingCount, noun)}\n            </Button>\n          ) : null}\n          {onShowSelectedOnlyChange && !narrow ? (\n            <label\n              data-slot=\"data-table-show-selected-only\"\n              className=\"ml-2 flex items-center gap-2 text-sm whitespace-nowrap text-foreground\"\n            >\n              <Checkbox\n                checked={showSelectedOnly}\n                onCheckedChange={(checked) => onShowSelectedOnlyChange(checked)}\n              />\n              Show selected only\n            </label>\n          ) : null}\n        </>\n      ) : null}\n      {children}\n    </div>\n  )\n}\n\n/**\n * Search, sort, group and filters, on the left while nothing is selected.\n * Narrow, the search stays and everything else moves into a Filters sheet.\n */\nfunction DataTableFilters({\n  className,\n  search,\n  children,\n  ref,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  /** The search field, such as DataTableSearch. It stays in the toolbar at every width. */\n  search?: React.ReactNode\n}) {\n  const { narrow } = useDataTableRoot()\n  const { count } = useSelectionSummary()\n  const [open, setOpen] = React.useState(false)\n  const filtersRef = React.useRef<HTMLDivElement>(null)\n  const mergedFiltersRef = React.useMemo(\n    () => mergeRefs(filtersRef, ref),\n    [ref]\n  )\n  const wasOpen = React.useRef(false)\n\n  // Widening past 32rem takes the sheet away while it's open. Focus was in\n  // its portal, out of the table's sight, so bring it back to the filters.\n  React.useLayoutEffect(() => {\n    if (!narrow && wasOpen.current) {\n      setOpen(false)\n      const active = document.activeElement\n      if (!active || active === document.body) {\n        filtersRef.current\n          ?.querySelector<HTMLElement>(\"input, button, [role=combobox]\")\n          ?.focus()\n      }\n    }\n    wasOpen.current = narrow && open\n  }, [narrow, open])\n\n  if (count > 0) return null\n  const hasControls = React.Children.toArray(children).length > 0\n  return (\n    <div\n      data-slot=\"data-table-filters\"\n      className={cn(\n        \"flex min-w-0 flex-1 flex-wrap items-center gap-1.5 data-narrow:flex-nowrap\",\n        className\n      )}\n      data-narrow={narrow || undefined}\n      ref={mergedFiltersRef}\n      {...props}\n    >\n      {search ? (\n        <div\n          data-slot=\"data-table-search\"\n          data-narrow={narrow || undefined}\n          className=\"w-56 min-w-0 data-narrow:w-auto data-narrow:flex-1\"\n        >\n          {search}\n        </div>\n      ) : null}\n      {hasControls && narrow ? (\n        <Sheet side=\"bottom\" open={open} onOpenChange={setOpen}>\n          <SheetTrigger\n            data-slot=\"data-table-filters-trigger\"\n            render={\n              <Button variant=\"outline\" size=\"icon-sm\" aria-label=\"Filters\" />\n            }\n          >\n            <SlidersHorizontalIcon />\n          </SheetTrigger>\n          <SheetContent keepMounted>\n            <SheetHeader>\n              <SheetTitle>Filters</SheetTitle>\n            </SheetHeader>\n            <SheetBody\n              data-slot=\"data-table-filters-sheet\"\n              className=\"flex flex-col items-start gap-3 pb-6 [&>[data-slot=input]]:w-full [&>[data-slot=select-trigger]]:w-full\"\n            >\n              {children}\n            </SheetBody>\n          </SheetContent>\n        </Sheet>\n      ) : (\n        children\n      )}\n    </div>\n  )\n}\n\n/**\n * A search field bound to the table's global filter, for the Filters\n * `search` slot. It searches every column with text or numbers in it.\n */\nfunction DataTableSearch({\n  className,\n  ...props\n}: Omit<\n  React.ComponentProps<typeof InputGroupInput>,\n  \"value\" | \"defaultValue\" | \"onChange\" | \"type\" | \"render\"\n>) {\n  const { table } = useDataTableRoot()\n  const value = useSelector(table.atoms.globalFilter, (filter) =>\n    typeof filter === \"string\" ? filter : \"\"\n  )\n  return (\n    <InputGroup data-slot=\"data-table-search-field\" size=\"sm\">\n      <InputGroupAddon>\n        <SearchIcon aria-hidden=\"true\" />\n      </InputGroupAddon>\n      <InputGroupInput\n        type=\"search\"\n        className={className}\n        value={value}\n        onChange={(event) => table.setGlobalFilter(event.target.value)}\n        {...props}\n      />\n    </InputGroup>\n  )\n}\n\n/** One value a column holds and how many rows hold it, for a filter menu. */\ntype DataTableFacet = { value: unknown; count: number }\n\n/**\n * The values a column holds and how many rows hold each, under the other\n * filters but not its own, so a filter menu keeps every choice it offers.\n */\nfunction useDataTableFacets(columnId: string): DataTableFacet[] {\n  const { table } = useDataTableRoot()\n  useSelector(\n    table.store,\n    (state) => [state.columnFilters, state.globalFilter],\n    { compare: shallow }\n  )\n  const facets = table.getColumn(columnId)?.getFacetedUniqueValues()\n  return [...(facets ?? new Map<unknown, number>())]\n    .map(([value, count]) => ({ value, count }))\n    .sort((a, b) => String(a.value).localeCompare(String(b.value)))\n}\n\n/**\n * A filter menu for one column, with how many rows hold each value. Give the\n * column `filterFn: \"arrHas\"`, so a row matches any value that's ticked.\n */\nfunction DataTableFacetFilter({\n  column: columnId,\n  label,\n  className,\n  ...props\n}: Omit<React.ComponentProps<typeof Button>, \"children\"> & {\n  /** The id of the column to filter. */\n  column: string\n  /** The menu's name. Defaults to the column's label or header. */\n  label?: string\n}) {\n  const { table } = useDataTableRoot()\n  const facets = useDataTableFacets(columnId)\n  const ticked = useSelector(table.atoms.columnFilters, (filters) => {\n    const value = filters.find((filter) => filter.id === columnId)?.value\n    return Array.isArray(value) ? (value as unknown[]) : []\n  })\n  const column = table.getColumn(columnId)\n  const name = label ?? (column ? labelOf(column) : columnId)\n\n  const toggle = (value: unknown, checked: boolean) =>\n    column?.setFilterValue((old: unknown) => {\n      const list = Array.isArray(old) ? (old as unknown[]) : []\n      const next = checked\n        ? [...list, value]\n        : list.filter((item) => item !== value)\n      return next.length ? next : undefined\n    })\n\n  return (\n    <Menu>\n      <MenuTrigger\n        data-slot=\"data-table-facet-filter\"\n        render={\n          <Button\n            variant=\"outline\"\n            size=\"sm\"\n            className={cn(\"data-filtered:border-ring\", className)}\n            data-filtered={ticked.length > 0 || undefined}\n            {...props}\n          />\n        }\n      >\n        <ListFilterIcon data-icon=\"inline-start\" />\n        {name}\n        {ticked.length ? (\n          <Count\n            value={ticked.length}\n            label={(n) => `, ${n} selected`}\n            className=\"rounded-sm bg-muted px-1 text-xs\"\n          />\n        ) : null}\n      </MenuTrigger>\n      <MenuContent>\n        <MenuGroup>\n          <MenuLabel>{name}</MenuLabel>\n          {facets.map(({ value, count }) => (\n            <MenuCheckboxItem\n              key={String(value)}\n              checked={ticked.includes(value)}\n              onCheckedChange={(checked) => toggle(value, checked)}\n            >\n              <span className=\"flex-1 truncate\">{String(value)}</span>{\" \"}\n              <Count\n                value={count}\n                className=\"ml-auto text-xs text-muted-foreground\"\n              />\n            </MenuCheckboxItem>\n          ))}\n        </MenuGroup>\n      </MenuContent>\n    </Menu>\n  )\n}\n\nfunction textOf(node: React.ReactNode): string | undefined {\n  if (typeof node === \"string\" || typeof node === \"number\") return String(node)\n  if (Array.isArray(node)) {\n    const text = node\n      .map((child) => textOf(child as React.ReactNode))\n      .filter(Boolean)\n      .join(\" \")\n      .trim()\n    return text || undefined\n  }\n  return undefined\n}\n\n/**\n * A create action, such as Add member. With an icon, it shrinks to the icon\n * on a narrow table, and its label becomes the name and a tooltip.\n */\nfunction DataTableAction({\n  icon,\n  children,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof Button> & {\n  /** Shown before the label, and alone on a narrow table. */\n  icon?: React.ReactNode\n}) {\n  const { narrow } = useDataTableRoot()\n  const label = textOf(children)\n  const iconOnly = narrow && Boolean(icon) && Boolean(label)\n  // One element tree at every width, so crossing 32rem doesn't remount the\n  // button and drop its focus.\n  return (\n    <Tooltip disabled={!iconOnly}>\n      <TooltipTrigger\n        render={\n          <Button\n            data-slot=\"data-table-action\"\n            variant={variant}\n            size={iconOnly ? \"icon-sm\" : \"sm\"}\n            aria-label={iconOnly ? label : undefined}\n            {...props}\n          />\n        }\n      >\n        {icon}\n        {iconOnly ? null : children}\n      </TooltipTrigger>\n      <TooltipContent>{label}</TooltipContent>\n    </Tooltip>\n  )\n}\n\n/** Create actions, such as Add member, on the right while nothing is selected. */\nfunction DataTableActions({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  const { count } = useSelectionSummary()\n  if (count > 0) return null\n  return (\n    <div\n      data-slot=\"data-table-actions\"\n      className={cn(\"ml-auto flex items-center gap-1.5\", className)}\n      {...props}\n    />\n  )\n}\n\n/**\n * Shows and hides columns, from a menu of every column that can hide. It\n * goes in DataTableActions; narrow, it shrinks to its icon.\n */\nfunction DataTableColumns({\n  label = \"Columns\",\n  ...props\n}: Omit<React.ComponentProps<typeof Button>, \"children\"> & {\n  /** The button's label, and its name when it shows only its icon. */\n  label?: string\n}) {\n  const { table, narrow } = useDataTableRoot()\n  const visibility = useSelector(table.atoms.columnVisibility)\n  const columns = table\n    .getAllLeafColumns()\n    .filter((column) => column.getCanHide())\n  return (\n    <Menu>\n      <MenuTrigger\n        data-slot=\"data-table-columns\"\n        render={\n          <Button\n            variant=\"outline\"\n            size={narrow ? \"icon-sm\" : \"sm\"}\n            aria-label={narrow ? label : undefined}\n            {...props}\n          />\n        }\n      >\n        <Columns3Icon data-icon={narrow ? undefined : \"inline-start\"} />\n        {narrow ? null : label}\n      </MenuTrigger>\n      <MenuContent align=\"end\">\n        <MenuGroup>\n          <MenuLabel>Show columns</MenuLabel>\n          {columns.map((column) => (\n            <MenuCheckboxItem\n              key={column.id}\n              checked={visibility[column.id] !== false}\n              onCheckedChange={(checked) => column.toggleVisibility(checked)}\n            >\n              {labelOf(column)}\n            </MenuCheckboxItem>\n          ))}\n        </MenuGroup>\n      </MenuContent>\n    </Menu>\n  )\n}\n\n/*\n * The order never changes: your actions, then More, then Delete behind a\n * divider. That's what makes every table's bulk actions read the same.\n */\nfunction DataTableBulkActions({\n  className,\n  children,\n  moreActions,\n  onDelete,\n  deleteLabel = \"Delete\",\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  /** Menu items for actions that don't earn a button of their own. */\n  moreActions?: React.ReactNode\n  /** Shows Delete, last and destructive, and runs this when it's pressed. */\n  onDelete?: () => void\n  /** Delete's label, such as “Remove” or “Archive”. */\n  deleteLabel?: string\n}) {\n  const { narrow, noun } = useDataTableRoot()\n  const { count, hiddenCount } = useSelectionSummary()\n  const countId = React.useId()\n  if (count === 0) return null\n  const hasOthers =\n    React.Children.toArray(children).length > 0 || Boolean(moreActions)\n  // Every action reaches every selected row, hidden ones too, so Delete\n  // says how many before it's pressed.\n  const reach = `${countLabel(count, noun)}${\n    hiddenCount > 0\n      ? `, including ${hiddenCount.toLocaleString(\"en-US\")} hidden by filters`\n      : \"\"\n  }`\n  return (\n    <div\n      role=\"group\"\n      aria-label={`Actions on ${countLabel(count, noun)}`}\n      data-slot=\"data-table-bulk-actions\"\n      className={cn(\"ml-auto flex items-center gap-1.5\", className)}\n      {...props}\n    >\n      {children}\n      {moreActions ? (\n        <Menu>\n          <MenuTrigger\n            render={\n              <Button\n                variant=\"outline\"\n                size=\"icon-sm\"\n                aria-label=\"More actions\"\n              />\n            }\n          >\n            <EllipsisIcon />\n          </MenuTrigger>\n          <MenuContent align=\"end\">{moreActions}</MenuContent>\n        </Menu>\n      ) : null}\n      {onDelete ? (\n        <>\n          {hasOthers ? (\n            <Separator\n              orientation=\"vertical\"\n              className=\"mx-1 h-5 self-center\"\n            />\n          ) : null}\n          <span id={countId} hidden>\n            {reach}\n          </span>\n          {narrow ? (\n            <Button\n              data-slot=\"data-table-delete\"\n              variant=\"destructive\"\n              size=\"icon-sm\"\n              aria-label={deleteLabel}\n              aria-describedby={countId}\n              onClick={onDelete}\n            >\n              <Trash2Icon />\n            </Button>\n          ) : (\n            <Button\n              data-slot=\"data-table-delete\"\n              variant=\"destructive\"\n              size=\"sm\"\n              aria-describedby={countId}\n              onClick={onDelete}\n            >\n              <Trash2Icon data-icon=\"inline-start\" />\n              {deleteLabel}\n            </Button>\n          )}\n        </>\n      ) : null}\n    </div>\n  )\n}\n\n/**\n * A bulk action button. With `single`, it only works on one row: it stays in\n * place, dimmed, while more than one is selected.\n */\nfunction DataTableBulkAction({\n  className,\n  single = false,\n  icon,\n  disabled,\n  children,\n  \"aria-describedby\": describedBy,\n  ...props\n}: React.ComponentProps<typeof Button> & {\n  /** Works on one row at a time, such as Edit or Duplicate. */\n  single?: boolean\n  /** Shown before the label, and alone on a narrow table. */\n  icon?: React.ReactNode\n}) {\n  const { noun, narrow } = useDataTableRoot()\n  const { count } = useSelectionSummary()\n  const reasonId = React.useId()\n  const tooMany = single && count !== 1\n  const reason = `Works on one ${noun.one} at a time`\n  const label = textOf(children)\n  const iconOnly = narrow && Boolean(icon) && Boolean(label)\n  // Always inside the Tooltip, so crossing one selected row doesn't remount\n  // the button and lose its focus.\n  return (\n    <Tooltip disabled={!tooMany && !iconOnly}>\n      <TooltipTrigger\n        render={\n          <Button\n            data-slot=\"data-table-bulk-action\"\n            variant=\"outline\"\n            size={iconOnly ? \"icon-sm\" : \"sm\"}\n            aria-label={iconOnly ? label : undefined}\n            disabled={disabled || tooMany}\n            focusableWhenDisabled={single}\n            aria-describedby={\n              [describedBy, tooMany ? reasonId : undefined]\n                .filter(Boolean)\n                .join(\" \") || undefined\n            }\n            className={cn(\"data-disabled:opacity-50\", className)}\n            {...props}\n          />\n        }\n      >\n        {icon}\n        {iconOnly ? null : children}\n      </TooltipTrigger>\n      <TooltipContent>\n        {iconOnly && tooMany\n          ? `${label}: ${reason.toLowerCase()}`\n          : tooMany\n            ? reason\n            : label}\n      </TooltipContent>\n      {tooMany ? (\n        <span id={reasonId} hidden>\n          {reason}\n        </span>\n      ) : null}\n    </Tooltip>\n  )\n}\n\n/**\n * The table itself. With no children it renders the header and the rows\n * from the table's columns.\n */\nfunction DataTableContent({\n  className,\n  children,\n  ref,\n  ...props\n}: React.ComponentProps<typeof Table>) {\n  const { cards, narrowLayout, loading } = useDataTableRoot()\n  const tableRef = React.useRef<HTMLTableElement>(null)\n  const mergedTableRef = React.useMemo(() => mergeRefs(tableRef, ref), [ref])\n\n  // Scroll shadows: mark the edges a pinned column covers while there's more\n  // table under them. It lives here so a table that mounts late still gets it.\n  React.useEffect(() => {\n    const table = tableRef.current\n    const scroller = table?.parentElement\n    const root = table?.closest<HTMLElement>('[data-slot=\"data-table\"]')\n    if (!table || !scroller || !root) return\n    const update = () => {\n      const left = Math.abs(scroller.scrollLeft)\n      const max = scroller.scrollWidth - scroller.clientWidth\n      root.toggleAttribute(\"data-scroll-start\", left > 0)\n      root.toggleAttribute(\"data-scroll-end\", max - left > 1)\n    }\n    update()\n    scroller.addEventListener(\"scroll\", update, { passive: true })\n    const observer = new ResizeObserver(update)\n    observer.observe(scroller)\n    observer.observe(table)\n    return () => {\n      scroller.removeEventListener(\"scroll\", update)\n      observer.disconnect()\n      root.removeAttribute(\"data-scroll-start\")\n      root.removeAttribute(\"data-scroll-end\")\n    }\n  }, [cards])\n\n  // Only one of the table and the cards is in the DOM, so screen readers\n  // never meet every row twice.\n  if (cards) return null\n\n  return (\n    <Table\n      ref={mergedTableRef}\n      data-slot=\"data-table-content\"\n      aria-busy={loading || undefined}\n      className={cn(\n        \"table-fixed\",\n        // Before the first measurement, CSS keeps a narrow cards table from\n        // flashing its rows.\n        narrowLayout === \"cards\" && \"@max-lg/data-table:hidden\",\n        className\n      )}\n      {...props}\n    >\n      {children ?? (\n        <>\n          <DataTableHeader />\n          <DataTableBody />\n        </>\n      )}\n    </Table>\n  )\n}\n\n/*\n * Pinning switches off under 32rem, the container's @lg size, where a pinned\n * column would leave no room for the rest.\n */\nconst pinnedClasses: Record<DataTablePinned, string> = {\n  none: \"\",\n  start:\n    \"@lg/data-table:sticky @lg/data-table:left-9 @lg/data-table:z-10 @lg/data-table:border-r after:pointer-events-none after:absolute after:inset-y-0 after:-right-3 after:w-3 after:bg-linear-to-r after:from-border after:to-transparent after:opacity-0 after:transition-opacity @lg/data-table:group-data-scroll-start/data-table:after:opacity-100\",\n  end: \"@lg/data-table:sticky @lg/data-table:right-0 @lg/data-table:z-10 @lg/data-table:border-l before:pointer-events-none before:absolute before:inset-y-0 before:-left-3 before:w-3 before:bg-linear-to-l before:from-border before:to-transparent before:opacity-0 before:transition-opacity @lg/data-table:group-data-scroll-end/data-table:before:opacity-100\",\n}\n\nconst selectColumnClasses =\n  \"w-9 pr-0 pl-3 @lg/data-table:sticky @lg/data-table:left-0 @lg/data-table:z-10\"\n\nfunction SelectAllCheckbox() {\n  const { table, noun } = useDataTableRoot()\n  const { pageState, selectableOnPage } = useSelectionSummary()\n  return (\n    <Checkbox\n      aria-label={`Select all ${noun.other} on this page`}\n      checked={pageState === \"all\"}\n      indeterminate={pageState === \"some\"}\n      disabled={selectableOnPage === 0}\n      onCheckedChange={() => table.toggleAllPageRowsSelected()}\n    />\n  )\n}\n\n/**\n * The header row, with select all first. With no children it renders a\n * head for every visible column, sortable ones as buttons.\n */\nfunction DataTableHeader({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof TableHeader>) {\n  if (children === undefined) {\n    return (\n      <TableHeader\n        data-slot=\"data-table-header\"\n        className={className}\n        {...props}\n      >\n        <ModelHeaderRows />\n      </TableHeader>\n    )\n  }\n  return (\n    <TableHeader data-slot=\"data-table-header\" className={className} {...props}>\n      <TableRow>\n        <TableHead\n          data-slot=\"data-table-select-all\"\n          scope=\"col\"\n          className={selectColumnClasses}\n        >\n          <SelectAllCheckbox />\n        </TableHead>\n        {children}\n      </TableRow>\n    </TableHeader>\n  )\n}\n\nfunction ModelHeaderRows() {\n  const { table } = useDataTableRoot()\n  useSelector(\n    table.store,\n    (state) => [state.sorting, state.columnVisibility, state.columnPinning],\n    { compare: shallow }\n  )\n  const groups = table.getHeaderGroups()\n  return groups.map((group, index) => (\n    <TableRow key={group.id}>\n      {index === 0 ? (\n        <TableHead\n          data-slot=\"data-table-select-all\"\n          scope=\"col\"\n          rowSpan={groups.length > 1 ? groups.length : undefined}\n          className={selectColumnClasses}\n        >\n          <SelectAllCheckbox />\n        </TableHead>\n      ) : null}\n      {group.headers.map((header) => (\n        <ModelHead key={header.id} header={header} />\n      ))}\n    </TableRow>\n  ))\n}\n\nconst sortButtonClasses =\n  \"-mx-1 inline-flex h-6 max-w-full items-center gap-1 rounded-sm px-1 font-medium outline-none hover:bg-background focus-visible:ring-[3px] focus-visible:ring-ring-subtle [&_svg]:size-3.5 [&_svg]:shrink-0\"\n\n/*\n * A sortable head holds a button, and only the sorted one carries aria-sort,\n * so a screen reader hears the order once, where it applies.\n */\nfunction ModelHead({\n  header,\n}: {\n  header: Header<DataTableFeatures, RowData, unknown>\n}) {\n  const column = header.column\n  const meta = column.columnDef.meta\n  const sorted = column.getIsSorted()\n  const content = header.isPlaceholder ? null : <FlexRender header={header} />\n  return (\n    <HeadCell\n      type={meta?.type}\n      pinned={pinnedOf(column)}\n      className={meta?.className}\n      colSpan={header.colSpan > 1 ? header.colSpan : undefined}\n      scope={header.colSpan > 1 ? \"colgroup\" : \"col\"}\n      aria-sort={\n        sorted === \"asc\"\n          ? \"ascending\"\n          : sorted === \"desc\"\n            ? \"descending\"\n            : undefined\n      }\n    >\n      {!header.isPlaceholder && column.getCanSort() ? (\n        <button\n          type=\"button\"\n          data-slot=\"data-table-sort\"\n          className={sortButtonClasses}\n          onClick={column.getToggleSortingHandler()}\n        >\n          <span className=\"truncate\">{content}</span>\n          {sorted === \"asc\" ? (\n            <ArrowUpIcon aria-hidden=\"true\" />\n          ) : sorted === \"desc\" ? (\n            <ArrowDownIcon aria-hidden=\"true\" />\n          ) : (\n            <ChevronsUpDownIcon\n              aria-hidden=\"true\"\n              className=\"text-muted-foreground\"\n            />\n          )}\n        </button>\n      ) : (\n        content\n      )}\n    </HeadCell>\n  )\n}\n\ntype HeadCellProps = React.ComponentProps<typeof TableHead> & {\n  /** The same type as the column's cells, so the label lines up with them. */\n  type?: DataTableCellType\n  /** The same pinned as the column's cells. */\n  pinned?: DataTablePinned\n}\n\nfunction HeadCell({\n  className,\n  type = \"text\",\n  pinned = \"none\",\n  ...props\n}: HeadCellProps) {\n  return (\n    <TableHead\n      data-slot=\"data-table-head\"\n      scope=\"col\"\n      data-type={type}\n      data-pinned={pinned === \"none\" ? undefined : pinned}\n      className={cn(\n        \"data-[type=actions]:w-14 data-[type=numeric]:w-28 data-[type=numeric]:text-right data-[type=status]:w-28\",\n        pinnedClasses[pinned],\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/** The rows. With no children it renders the table's current page. */\nfunction DataTableBody({\n  children,\n  ...props\n}: React.ComponentProps<typeof TableBody>) {\n  return (\n    <TableBody data-slot=\"data-table-body\" {...props}>\n      {children === undefined ? <ModelRows /> : children}\n    </TableBody>\n  )\n}\n\n// What changes which rows show and which cells they have. Selection isn't\n// here: a ticked row re-renders itself, not the body.\nfunction useRowModelInputs() {\n  const { table } = useDataTableRoot()\n  return useSelector(\n    table.store,\n    (state) => [\n      state.sorting,\n      state.columnFilters,\n      state.globalFilter,\n      state.pagination,\n      state.columnVisibility,\n      state.columnPinning,\n    ],\n    { compare: shallow }\n  )\n}\n\nfunction ModelRows() {\n  const { table, empty, loading } = useDataTableRoot()\n  const inputs = useRowModelInputs()\n  // TanStack keeps a row's object until the data changes, so new columns or\n  // a new lockedReason over the same data have to reach the memo as props.\n  const columns = table.getVisibleLeafColumns()\n  const lockedReason = table.options.meta?.lockedReason\n  if (loading) return <DataTableSkeleton />\n  const { rows } = table.getRowModel()\n  if (rows.length === 0) {\n    return empty ? (\n      <TableRow\n        data-slot=\"data-table-empty-row\"\n        className=\"hover:bg-transparent\"\n      >\n        {/* One more for the selection column. */}\n        <TableCell colSpan={columns.length + 1} className=\"p-0\">\n          {/* The table can be wider than its scroller, so the message keeps\n              to the visible width rather than centring on the whole row. */}\n          <div className=\"sticky left-0 w-[100cqw] whitespace-normal\">\n            {empty}\n          </div>\n        </TableCell>\n      </TableRow>\n    ) : null\n  }\n  return rows.map((row) => (\n    <ModelRow\n      key={row.id}\n      row={row}\n      inputs={inputs}\n      columns={columns}\n      lockedReason={lockedReason?.(row.original)}\n    />\n  ))\n}\n\nconst ModelRow = React.memo(function ModelRow({\n  row,\n  lockedReason,\n}: {\n  row: DataTableAnyRow\n  /** Changes when visibility or pinning does, so the memo lets those through. */\n  inputs: unknown\n  /** The visible columns, which change when the column definitions do. */\n  columns: unknown\n  /** Why the row can't be selected, as a value, so an inline function doesn't redraw every row. */\n  lockedReason: React.ReactNode\n}) {\n  // Its own selection redraws its cells, so a renderer that reads\n  // row.getIsSelected() follows the checkbox; other rows stay put.\n  useRowSelected(row.id)\n  return (\n    <RowFrame id={row.id} lockedReason={lockedReason}>\n      {row.getVisibleCells().map((cell) => {\n        const meta = cell.column.columnDef.meta\n        return (\n          <BodyCell\n            key={cell.id}\n            type={meta?.type}\n            pinned={pinnedOf(cell.column)}\n            className={meta?.className}\n            secondary={meta?.secondary?.(row.original)}\n            avatar={meta?.avatar?.(row.original)}\n            icon={meta?.icon}\n          >\n            <FlexRender cell={cell} />\n          </BodyCell>\n        )\n      })}\n    </RowFrame>\n  )\n})\n\nfunction RowFrame({\n  id,\n  lockedReason,\n  children,\n}: {\n  /** The row's id, the same one getRowId gives it. */\n  id: string\n  /** Why the row can't be selected. Setting it locks the row. */\n  lockedReason?: React.ReactNode\n  children: React.ReactNode\n}) {\n  const { table } = useDataTableRoot()\n  const labelId = React.useId()\n  const selectId = React.useId()\n  const reasonId = React.useId()\n  const locked = hasReason(lockedReason)\n  const selected = useRowSelected(id)\n\n  // The first primary or person cell names the row's checkbox. Only one\n  // element can carry the id, so the row hands it out after render.\n  const rowRef = React.useRef<HTMLTableRowElement>(null)\n  React.useLayoutEffect(() => {\n    const cell = rowRef.current?.querySelector(\"[data-row-name]\")\n    if (cell && cell.id !== labelId) cell.id = labelId\n  })\n\n  const row = React.useMemo(\n    () => ({ id, labelId, reasonId, lockedReason }),\n    [id, labelId, reasonId, lockedReason]\n  )\n\n  return (\n    <DataTableRowContext.Provider value={row}>\n      <TableRow\n        ref={rowRef}\n        data-slot=\"data-table-row\"\n        data-row-id={id}\n        data-selected={selected || undefined}\n        data-locked={locked || undefined}\n        className=\"group/row\"\n      >\n        <TableCell\n          data-slot=\"data-table-select\"\n          className={selectColumnClasses}\n        >\n          <Checkbox\n            aria-labelledby={`${selectId} ${labelId}`}\n            aria-describedby={locked ? reasonId : undefined}\n            checked={selected}\n            disabled={locked}\n            onCheckedChange={(checked, details) =>\n              toggleRow(table, id, checked, shiftKeyOf(details))\n            }\n          />\n          <span id={selectId} hidden>\n            Select\n          </span>\n          {locked ? (\n            <span id={reasonId} hidden>\n              {lockedReason}\n            </span>\n          ) : null}\n        </TableCell>\n        {children}\n      </TableRow>\n    </DataTableRowContext.Provider>\n  )\n}\n\nfunction DataTableLock() {\n  const row = React.useContext(DataTableRowContext)\n  if (!row || !hasReason(row.lockedReason)) return null\n  return (\n    <Tooltip>\n      <TooltipTrigger\n        data-slot=\"data-table-lock\"\n        className=\"inline-flex size-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring-subtle\"\n        aria-label=\"Locked\"\n        aria-describedby={row.reasonId}\n      >\n        <LockIcon className=\"size-3.5\" />\n      </TooltipTrigger>\n      <TooltipContent>{row.lockedReason}</TooltipContent>\n    </Tooltip>\n  )\n}\n\ntype BodyCellProps = React.ComponentProps<typeof TableCell> & {\n  /** What the column holds. The same type as its header. */\n  type?: DataTableCellType\n  /** Which edge the column sticks to. The same pinned as its header. */\n  pinned?: DataTablePinned\n  /** A second, smaller line under the value. Makes the row 52px tall. */\n  secondary?: React.ReactNode\n  /** An icon before the value, on a primary cell. */\n  icon?: React.ReactNode\n  /** The person's photo on a person cell. Initials show while it loads. */\n  avatar?: DataTableAvatar\n}\n\nfunction BodyCell({\n  className,\n  type = \"text\",\n  pinned = \"none\",\n  secondary,\n  icon,\n  avatar,\n  children,\n  ...props\n}: BodyCellProps) {\n  const names = type === \"primary\" || type === \"person\"\n\n  const value =\n    type === \"actions\" ? (\n      children\n    ) : type === \"status\" ? (\n      // Badge tints are translucent; over a selected row's muted fill the\n      // warning tint drops under AA, so the badge sits on the background.\n      <span className=\"inline-flex rounded-full bg-background\">{children}</span>\n    ) : (\n      <span className=\"flex min-w-0 flex-col\">\n        <span\n          data-row-name={names || undefined}\n          className={cn(\n            \"truncate\",\n            names && \"font-medium\",\n            type === \"numeric\" &&\n              \"text-muted-foreground tabular-nums group-hover/row:text-foreground group-data-selected/row:text-foreground\"\n          )}\n        >\n          {children}\n        </span>\n        {secondary ? (\n          <span\n            data-slot=\"data-table-secondary\"\n            className=\"truncate text-xs text-muted-foreground group-hover/row:text-foreground group-data-selected/row:text-foreground\"\n          >\n            {secondary}\n          </span>\n        ) : null}\n      </span>\n    )\n\n  return (\n    <TableCell\n      data-slot=\"data-table-cell\"\n      data-type={type}\n      data-pinned={pinned === \"none\" ? undefined : pinned}\n      className={cn(\n        \"data-[type=actions]:pr-3 data-[type=actions]:text-right data-[type=numeric]:text-right\",\n        pinnedClasses[pinned],\n        className\n      )}\n      {...props}\n    >\n      <div\n        className={cn(\n          \"flex items-center gap-2\",\n          (type === \"numeric\" || type === \"actions\") && \"justify-end\"\n        )}\n      >\n        {type === \"primary\" && icon ? (\n          <span className=\"shrink-0 text-muted-foreground [&_svg]:size-4\">\n            {icon}\n          </span>\n        ) : null}\n        {type === \"person\" && avatar ? (\n          <Avatar size=\"sm\">\n            {avatar.src ? <AvatarImage src={avatar.src} alt=\"\" /> : null}\n            <AvatarFallback>{avatar.fallback}</AvatarFallback>\n          </Avatar>\n        ) : null}\n        {value}\n        {names ? <DataTableLock /> : null}\n      </div>\n    </TableCell>\n  )\n}\n\n/**\n * The rows as cards, shown instead of the table when narrowLayout is\n * \"cards\". With no children it renders the table's current page: the\n * primary or person column as the title, status at the end, and the other\n * columns as fields. A function child renders each row's card your way.\n */\nfunction DataTableCards({\n  className,\n  children,\n  ...props\n}: Omit<React.ComponentProps<\"ul\">, \"children\"> & {\n  /** Cards written by hand, or a function that renders one for each row. */\n  children?: React.ReactNode | ((row: DataTableAnyRow) => React.ReactNode)\n}) {\n  const { cards, loading } = useDataTableRoot()\n  if (!cards) return null\n  return (\n    <ul\n      data-slot=\"data-table-cards\"\n      aria-busy={loading || undefined}\n      className={cn(\"flex flex-col\", className)}\n      {...props}\n    >\n      {children === undefined || typeof children === \"function\" ? (\n        <ModelCards render={children} />\n      ) : (\n        children\n      )}\n    </ul>\n  )\n}\n\nfunction ModelCards({\n  render,\n}: {\n  render?: (row: DataTableAnyRow) => React.ReactNode\n}) {\n  const { table, empty, loading } = useDataTableRoot()\n  useRowModelInputs()\n  if (loading) return <DataTableSkeleton />\n  const { rows } = table.getRowModel()\n  if (rows.length === 0) {\n    return empty ? <li data-slot=\"data-table-empty-card\">{empty}</li> : null\n  }\n  return rows.map((row) =>\n    render ? (\n      <React.Fragment key={row.id}>{render(row)}</React.Fragment>\n    ) : (\n      <ModelCard key={row.id} row={row} />\n    )\n  )\n}\n\nfunction ModelCard({ row }: { row: DataTableAnyRow }) {\n  // As in ModelRow: a renderer that reads row.getIsSelected() follows it.\n  useRowSelected(row.id)\n  const cells = row.getVisibleCells()\n  const typeOf = (cell: (typeof cells)[number]) =>\n    cell.column.columnDef.meta?.type ?? \"text\"\n  const title =\n    cells.find((cell) => [\"primary\", \"person\"].includes(typeOf(cell))) ??\n    cells[0]\n  const status = cells.find((cell) => typeOf(cell) === \"status\")\n  const fields = cells.filter(\n    (cell) => cell !== title && cell !== status && typeOf(cell) !== \"actions\"\n  )\n  return (\n    <DataTableCard\n      id={row.id}\n      title={title ? <FlexRender cell={title} /> : row.id}\n      avatar={title?.column.columnDef.meta?.avatar?.(row.original)}\n      status={status ? <FlexRender cell={status} /> : undefined}\n      lockedReason={row.table.options.meta?.lockedReason?.(row.original)}\n    >\n      {fields.length\n        ? fields.map((cell) => (\n            <DataTableCardField key={cell.id} label={labelOf(cell.column)}>\n              <FlexRender cell={cell} />\n            </DataTableCardField>\n          ))\n        : null}\n    </DataTableCard>\n  )\n}\n\nfunction DataTableCard({\n  className,\n  id,\n  title,\n  avatar,\n  status,\n  lockedReason,\n  children,\n  ...props\n}: Omit<React.ComponentProps<\"li\">, \"id\" | \"title\"> & {\n  /** The row's id, the same one getRowId gives it. */\n  id: string\n  /** The row's name. It also names the card's checkbox. */\n  title: React.ReactNode\n  /** The person's photo, for a row that's a person. */\n  avatar?: DataTableAvatar\n  /** A status Badge, at the end of the heading. */\n  status?: React.ReactNode\n  /** Why the row can't be selected. Setting it locks the card. */\n  lockedReason?: React.ReactNode\n}) {\n  const { table } = useDataTableRoot()\n  const labelId = React.useId()\n  const selectId = React.useId()\n  const reasonId = React.useId()\n  const locked = hasReason(lockedReason)\n  const selected = useRowSelected(id)\n\n  const row = React.useMemo(\n    () => ({ id, labelId, reasonId, lockedReason }),\n    [id, labelId, reasonId, lockedReason]\n  )\n\n  return (\n    <DataTableRowContext.Provider value={row}>\n      <li\n        data-slot=\"data-table-card\"\n        data-row-id={id}\n        data-selected={selected || undefined}\n        data-locked={locked || undefined}\n        className={cn(\n          \"group/row flex gap-3 border-b border-border bg-background p-3 last:border-b-0 data-selected:bg-muted\",\n          className\n        )}\n        {...props}\n      >\n        <span className=\"flex pt-0.5\">\n          <Checkbox\n            aria-labelledby={`${selectId} ${labelId}`}\n            aria-describedby={locked ? reasonId : undefined}\n            checked={selected}\n            disabled={locked}\n            onCheckedChange={(checked, details) =>\n              toggleRow(table, id, checked, shiftKeyOf(details))\n            }\n          />\n          <span id={selectId} hidden>\n            Select\n          </span>\n          {locked ? (\n            <span id={reasonId} hidden>\n              {lockedReason}\n            </span>\n          ) : null}\n        </span>\n        <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n          <div className=\"flex min-h-6 items-center gap-2\">\n            {avatar ? (\n              <Avatar size=\"sm\">\n                {avatar.src ? <AvatarImage src={avatar.src} alt=\"\" /> : null}\n                <AvatarFallback>{avatar.fallback}</AvatarFallback>\n              </Avatar>\n            ) : null}\n            <span id={labelId} className=\"truncate text-sm font-medium\">\n              {title}\n            </span>\n            <DataTableLock />\n            {status ? (\n              <span className=\"ml-auto inline-flex shrink-0 rounded-full bg-background\">\n                {status}\n              </span>\n            ) : null}\n          </div>\n          {children ? (\n            <dl className=\"grid grid-cols-2 gap-x-4 gap-y-1.5\">{children}</dl>\n          ) : null}\n        </div>\n      </li>\n    </DataTableRowContext.Provider>\n  )\n}\n\nfunction DataTableCardField({\n  className,\n  label,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  /** What the value is, such as “Team”. */\n  label: React.ReactNode\n}) {\n  return (\n    <div\n      data-slot=\"data-table-card-field\"\n      className={cn(\"flex min-w-0 flex-col\", className)}\n      {...props}\n    >\n      <dt className=\"truncate text-xs text-muted-foreground group-data-selected/row:text-foreground\">\n        {label}\n      </dt>\n      <dd className=\"truncate text-sm\">{children}</dd>\n    </div>\n  )\n}\n\n// Varied so the rows read as text rather than a grid of equal bars.\nconst SKELETON_WIDTHS = [\"w-3/4\", \"w-1/2\", \"w-2/3\", \"w-2/5\"]\n\n/**\n * Placeholder rows shaped like the visible columns, or placeholder cards\n * on a narrow cards table. The body and the cards show it while `loading`\n * is set; a hand-written body can render it too. Hidden from screen\n * readers: the table is aria-busy instead.\n */\nfunction DataTableSkeleton({\n  rows = 5,\n}: {\n  /** How many placeholder rows or cards. */\n  rows?: number\n}) {\n  const { table, cards } = useDataTableRoot()\n  useRowModelInputs()\n  const columns = table.getVisibleLeafColumns()\n  const indexes = Array.from({ length: rows }, (_, i) => i)\n\n  if (cards) {\n    // As many fields as ModelCard lays out, so the cards keep their height\n    // when the data arrives.\n    const typeOf = (column: DataTableAnyColumn) =>\n      column.columnDef.meta?.type ?? \"text\"\n    const title =\n      columns.find((column) =>\n        [\"primary\", \"person\"].includes(typeOf(column))\n      ) ?? columns[0]\n    const fields = columns.filter(\n      (column) =>\n        column !== title &&\n        typeOf(column) !== \"status\" &&\n        typeOf(column) !== \"actions\"\n    ).length\n    return indexes.map((i) => (\n      <li\n        key={i}\n        aria-hidden=\"true\"\n        data-slot=\"data-table-skeleton\"\n        className=\"flex gap-3 border-b border-border p-3 last:border-b-0\"\n      >\n        <span className=\"flex pt-0.5\">\n          <Skeleton className=\"size-4 rounded-sm\" />\n        </span>\n        <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n          <div className=\"flex h-6 items-center gap-2\">\n            {title && typeOf(title) === \"person\" ? (\n              <Skeleton className=\"size-6 shrink-0 rounded-full\" />\n            ) : null}\n            <Skeleton className={cn(\"h-3.5\", SKELETON_WIDTHS[i % 4])} />\n          </div>\n          {fields ? (\n            <div className=\"grid grid-cols-2 gap-x-4 gap-y-1.5\">\n              {Array.from({ length: fields }, (_, j) => (\n                <div key={j} className=\"flex flex-col\">\n                  <div className=\"flex h-4 items-center\">\n                    <Skeleton className=\"h-2.5 w-12\" />\n                  </div>\n                  <div className=\"flex h-5 items-center\">\n                    <Skeleton className=\"h-3.5 w-20\" />\n                  </div>\n                </div>\n              ))}\n            </div>\n          ) : null}\n        </div>\n      </li>\n    ))\n  }\n\n  return indexes.map((i) => (\n    <TableRow\n      key={i}\n      aria-hidden=\"true\"\n      data-slot=\"data-table-skeleton\"\n      className=\"[tbody>&]:hover:bg-background\"\n    >\n      <TableCell className={selectColumnClasses}>\n        <Skeleton className=\"size-4 rounded-sm\" />\n      </TableCell>\n      {columns.map((column, j) => {\n        const type = column.columnDef.meta?.type ?? \"text\"\n        const pinned = pinnedOf(column)\n        return (\n          <TableCell\n            key={column.id}\n            data-type={type}\n            data-pinned={pinned === \"none\" ? undefined : pinned}\n            className={cn(\n              pinnedClasses[pinned],\n              column.columnDef.meta?.className\n            )}\n          >\n            <div\n              className={cn(\n                // The heights of an avatar and of a row's action button,\n                // so the rows keep their height when the data arrives.\n                \"flex h-6 items-center gap-2\",\n                type === \"actions\" && \"h-8\",\n                (type === \"numeric\" || type === \"actions\") && \"justify-end\"\n              )}\n            >\n              {type === \"person\" ? (\n                <Skeleton className=\"size-6 shrink-0 rounded-full\" />\n              ) : null}\n              {type === \"status\" ? (\n                <Skeleton className=\"h-5 w-16 rounded-full\" />\n              ) : type === \"actions\" ? (\n                <Skeleton className=\"size-6\" />\n              ) : type === \"numeric\" ? (\n                <Skeleton className=\"h-3.5 w-10\" />\n              ) : (\n                <Skeleton\n                  className={cn(\"h-3.5\", SKELETON_WIDTHS[(i + j) % 4])}\n                />\n              )}\n            </div>\n          </TableCell>\n        )\n      })}\n    </TableRow>\n  ))\n}\n\n/**\n * What a table with no rows shows. With a search or filter on, it says\n * nothing matched and offers to clear them; otherwise it says there are no\n * rows yet. Children replace the copy and the button.\n */\nfunction DataTableEmpty({\n  children,\n  ...props\n}: React.ComponentProps<typeof EmptyState>) {\n  const { table, noun } = useDataTableRoot()\n  const filtered = useSelector(\n    table.store,\n    (state) => state.columnFilters.length > 0 || Boolean(state.globalFilter)\n  )\n  return (\n    <EmptyState data-slot=\"data-table-empty\" {...props}>\n      {children ?? (\n        <>\n          <EmptyStateTitle>\n            {filtered ? `No matching ${noun.other}` : `No ${noun.other} yet`}\n          </EmptyStateTitle>\n          {filtered ? (\n            <>\n              <EmptyStateDescription>\n                Try another search, or clear the filters.\n              </EmptyStateDescription>\n              <EmptyStateActions>\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  onClick={(event) => {\n                    // The button leaves with the empty state, so focus goes\n                    // to the search, where someone would try again.\n                    event.currentTarget\n                      .closest('[data-slot=\"data-table\"]')\n                      ?.querySelector<HTMLElement>(\n                        '[data-slot=\"data-table-search\"] input'\n                      )\n                      ?.focus()\n                    table.resetColumnFilters(true)\n                    table.setGlobalFilter(\"\")\n                  }}\n                >\n                  Clear filters\n                </Button>\n              </EmptyStateActions>\n            </>\n          ) : null}\n        </>\n      )}\n    </EmptyState>\n  )\n}\n\nfunction DataTableFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"data-table-footer\"\n      className={cn(\n        \"flex min-h-11 items-center justify-between gap-4 border-t border-border px-3 py-1.5 text-sm text-muted-foreground\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/**\n * Pagination wired to the table's pages, for the footer. Give the table a\n * page size, in `initialState.pagination` or a pagination atom.\n */\nfunction DataTablePagination(\n  props: Omit<\n    PaginationProps,\n    | \"page\"\n    | \"defaultPage\"\n    | \"pageCount\"\n    | \"onPageChange\"\n    | \"pageSize\"\n    | \"totalCount\"\n    | \"noun\"\n  >\n) {\n  const { table, noun } = useDataTableRoot()\n  const { pageIndex, pageSize } = useSelector(table.atoms.pagination)\n  useSelector(\n    table.store,\n    (state) => [state.columnFilters, state.globalFilter],\n    { compare: shallow }\n  )\n  return (\n    <Pagination\n      data-slot=\"data-table-pagination\"\n      page={pageIndex + 1}\n      pageCount={Math.max(1, table.getPageCount())}\n      onPageChange={(page) => table.setPageIndex(page - 1)}\n      // Every row on one page has no range to show, only NaN.\n      pageSize={Number.isFinite(pageSize) ? pageSize : undefined}\n      totalCount={table.getRowCount()}\n      noun={noun}\n      {...props}\n    />\n  )\n}\n\n/**\n * “2 of 6 members selected”, for a picker's footer, with “, 1 hidden by\n * filters” while a filter hides selected rows.\n */\nfunction DataTableSelectionCount({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  const { noun } = useDataTableRoot()\n  const { count, hiddenCount, totalCount } = useSelectionSummary()\n  return (\n    <span\n      data-slot=\"data-table-selection-count\"\n      className={cn(\"tabular-nums\", className)}\n      {...props}\n    >\n      {count.toLocaleString(\"en-US\")} of {countLabel(totalCount, noun)} selected\n      {hiddenSuffix(hiddenCount)}\n    </span>\n  )\n}\n\nexport {\n  createDataTableColumnHelper,\n  DataTable,\n  DataTableAction,\n  DataTableActions,\n  DataTableBody,\n  DataTableBulkAction,\n  DataTableBulkActions,\n  DataTableCard,\n  DataTableCardField,\n  DataTableCards,\n  DataTableEmpty,\n  DataTableSkeleton,\n  DataTableColumns,\n  DataTableContent,\n  dataTableCodecs,\n  DataTableFacetFilter,\n  dataTableFeatures,\n  DataTableFilters,\n  DataTableFooter,\n  DataTableHeader,\n  DataTablePagination,\n  DataTableSearch,\n  DataTableSelectionCount,\n  DataTableToolbar,\n  useDataTable,\n  useDataTableContext,\n  useDataTableFacets,\n  useSearchParamsAtom,\n}\nexport type {\n  DataTableAvatar,\n  DataTableCellType,\n  DataTableColumnMeta,\n  DataTableFacet,\n  DataTableFeatures,\n  DataTableInstance,\n  DataTableNarrowLayout,\n  DataTableNoun,\n  DataTableOptions,\n  DataTablePinned,\n  DataTableProps,\n  SearchParamCodec,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/lib/merge-refs.ts",
      "content": "import * as React from \"react\"\n\n/**\n * One callback ref that sets every ref given, so a part can keep its own\n * ref on an element and still hand the element to a caller's `ref`. Undo\n * runs through React 19's ref cleanup, which calls a function ref with\n * null and empties an object ref.\n */\nexport function mergeRefs<T>(\n  ...refs: (React.Ref<T> | undefined)[]\n): React.RefCallback<T> {\n  return (node) => {\n    const cleanups = refs.map((ref) => {\n      if (typeof ref === \"function\") {\n        const cleanup = ref(node)\n        return typeof cleanup === \"function\" ? cleanup : () => ref(null)\n      }\n      if (ref) {\n        ref.current = node\n        return () => {\n          ref.current = null\n        }\n      }\n      return undefined\n    })\n    return () => {\n      for (const cleanup of cleanups) cleanup?.()\n    }\n  }\n}\n",
      "type": "registry:lib"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ring-subtle": "var(--ring-subtle)"
    },
    "light": {
      "ring-subtle": "color-mix(in oklch, var(--color-neutral-400) 50%, transparent)"
    },
    "dark": {
      "ring-subtle": "color-mix(in oklch, var(--color-neutral-500) 50%, transparent)"
    }
  },
  "meta": {
    "tier": "base-components",
    "group": "Display",
    "docs": "https://fibo.toribryan.com/?path=/docs/base-components-display-data-table--docs"
  },
  "docs": "Docs and live examples: https://fibo.toribryan.com/?path=/docs/base-components-display-data-table--docs",
  "categories": [
    "base-components",
    "display"
  ],
  "type": "registry:ui"
}