{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table",
  "title": "Table",
  "description": "A sortable, selectable table with a sticky header, virtualized rows, pagination with a page-size select, and an infinite-scroll variant.",
  "dependencies": [
    "@hugeicons/core-free-icons",
    "@hugeicons/react",
    "@tanstack/react-virtual",
    "cn"
  ],
  "registryDependencies": [
    "https://ui.usenotra.com/r/notra-table.json",
    "https://ui.usenotra.com/r/notra-button.json",
    "https://ui.usenotra.com/r/notra-tooltip.json",
    "checkbox",
    "context-menu",
    "https://ui.usenotra.com/r/notra-select.json",
    "skeleton"
  ],
  "files": [
    {
      "path": "registry/notra/table/components/data-table-body.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport { Fragment } from \"react\";\n\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuTrigger,\n} from \"@/components/ui/context-menu\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nimport { TableBody, TableCell, TableRow } from \"../../table/components/table\";\nimport {\n  TABLE_CELL_INNER_CLASS,\n  TABLE_ROW_BORDER_CLASS,\n  TABLE_ROW_FOCUS_RING_CLASS,\n} from \"../constants/data-table\";\nimport { alignText, readCell } from \"../lib/data-table\";\nimport type {\n  DataTableBodyProps,\n  DataTableBodyRowProps,\n  TableColumn,\n} from \"../types/data-table\";\nimport { useDataTableLabels } from \"./data-table-labels\";\n\nconst INTERACTIVE_SELECTOR =\n  'button, a, input, select, textarea, label, [role=\"checkbox\"], [role=\"switch\"], [role=\"menuitem\"], [role=\"button\"], [contenteditable=\"true\"]';\n\nfunction isInteractiveTarget(target: EventTarget | null): boolean {\n  return (\n    target instanceof Element && target.closest(INTERACTIVE_SELECTOR) !== null\n  );\n}\n\nfunction SkeletonRows<T>({\n  count,\n  columns,\n  selectable,\n  rowHeight,\n  keyPrefix,\n}: {\n  count: number;\n  columns: TableColumn<T>[];\n  selectable: boolean;\n  rowHeight: number;\n  keyPrefix: string;\n}) {\n  return (\n    <>\n      {Array.from({ length: count }, (_, index) => (\n        <TableRow\n          className={TABLE_ROW_BORDER_CLASS}\n          key={`${keyPrefix}-${String(index)}`}\n          style={{ height: rowHeight }}\n        >\n          {selectable ? <TableCell className=\"h-auto p-0\" /> : null}\n          {columns.map((column) => (\n            <TableCell\n              className={cn(\"h-auto\", alignText(column.align))}\n              key={column.key}\n            >\n              <Skeleton\n                className={cn(\n                  \"h-3 rounded-full\",\n                  column.align === \"right\" ? \"ml-auto w-10\" : \"w-2/3\"\n                )}\n              />\n            </TableCell>\n          ))}\n          <TableCell aria-hidden className=\"h-auto p-0\" />\n        </TableRow>\n      ))}\n    </>\n  );\n}\n\nexport function DataTableBodyRow<T>({\n  className,\n  entry,\n  index,\n  isLastRow,\n  expanded,\n  detailId,\n  rowHeight,\n  rowSizing,\n  selectable,\n  isSelected,\n  columns,\n  onRowClick,\n  rowKeyboardActivation,\n  onRowPointerEnter,\n  onToggleRow,\n  renderRowContextMenu,\n}: DataTableBodyRowProps<T>) {\n  const labels = useDataTableLabels();\n  const contentSized = rowSizing === \"content\";\n  const cellBorder = isLastRow ? \"border-b-0\" : TABLE_ROW_BORDER_CLASS;\n  const keyboardActivation = Boolean(onRowClick) && rowKeyboardActivation;\n  const tableRow = (\n    <TableRow\n      className={cn(\n        \"group hover:bg-muted data-[selected=true]:bg-primary/5 scroll-my-8 outline-none\",\n        TABLE_ROW_FOCUS_RING_CLASS,\n        onRowClick && \"cursor-pointer\",\n        className\n      )}\n      aria-controls={detailId}\n      aria-expanded={expanded}\n      data-selected={isSelected}\n      onClick={\n        onRowClick\n          ? (event) => {\n              if (isInteractiveTarget(event.target)) {\n                return;\n              }\n              onRowClick(entry.row);\n            }\n          : undefined\n      }\n      onKeyDown={\n        onRowClick && keyboardActivation\n          ? (event) => {\n              if (event.target !== event.currentTarget) {\n                return;\n              }\n              if (event.key === \"Enter\" || event.key === \" \") {\n                event.preventDefault();\n                onRowClick(entry.row);\n              }\n            }\n          : undefined\n      }\n      onPointerEnter={\n        onRowPointerEnter ? () => onRowPointerEnter(entry.row) : undefined\n      }\n      style={contentSized ? { minHeight: rowHeight } : { height: rowHeight }}\n      tabIndex={keyboardActivation ? 0 : undefined}\n    >\n      {selectable ? (\n        <TableCell className={cn(\"h-auto p-0 text-center\", cellBorder)}>\n          <div className=\"flex items-center justify-center\">\n            <Checkbox\n              className=\"cursor-pointer\"\n              aria-label={labels.selectRow(String(index + 1))}\n              checked={isSelected}\n              onCheckedChange={() => onToggleRow(entry.id)}\n            />\n          </div>\n        </TableCell>\n      ) : null}\n      {columns.map((column) => (\n        <TableCell\n          className={cn(\n            \"h-auto max-w-0\",\n            contentSized\n              ? cn(\n                  \"overflow-visible\",\n                  column.verticalAlign === \"middle\"\n                    ? \"align-middle\"\n                    : \"align-top\"\n                )\n              : \"overflow-hidden\",\n            cellBorder,\n            alignText(column.align)\n          )}\n          key={column.key}\n        >\n          <div\n            className={cn(\n              TABLE_CELL_INNER_CLASS,\n              contentSized && \"overflow-visible py-3 whitespace-normal\",\n              alignText(column.align)\n            )}\n          >\n            {readCell(entry.row, column)}\n          </div>\n        </TableCell>\n      ))}\n      <TableCell aria-hidden className={cn(\"h-auto p-0\", cellBorder)} />\n    </TableRow>\n  );\n\n  if (!renderRowContextMenu) {\n    return tableRow;\n  }\n\n  return (\n    <ContextMenu>\n      <ContextMenuTrigger render={tableRow} />\n      <ContextMenuContent>{renderRowContextMenu(entry.row)}</ContextMenuContent>\n    </ContextMenu>\n  );\n}\n\nexport function DataTableBody<T>({\n  columns,\n  renderedRows,\n  rowCount,\n  rowHeight,\n  rowSizing,\n  bodyHeight,\n  loading,\n  loadingMore,\n  skeletonRows,\n  emptyState,\n  selectable,\n  selected,\n  scrolls,\n  paddingTop,\n  paddingBottom,\n  onToggleRow,\n  onRowClick,\n  rowKeyboardActivation,\n  isRowClickable,\n  getRowClassName,\n  onRowPointerEnter,\n  renderRowContextMenu,\n  renderRowDetail,\n}: DataTableBodyProps<T>) {\n  const colSpan = columns.length + (selectable ? 1 : 0) + 1;\n\n  if (rowCount === 0) {\n    return (\n      <TableBody>\n        {loading ? (\n          <SkeletonRows\n            columns={columns}\n            count={Math.max(1, Math.ceil(bodyHeight / rowHeight))}\n            keyPrefix=\"skeleton\"\n            rowHeight={rowHeight}\n            selectable={selectable}\n          />\n        ) : (\n          <TableRow>\n            <TableCell className=\"h-auto p-0\" colSpan={colSpan}>\n              <div\n                className=\"flex items-center justify-center px-6 text-center whitespace-normal\"\n                style={{ height: bodyHeight }}\n              >\n                {typeof emptyState === \"string\" ? (\n                  <span className=\"text-muted-foreground\">{emptyState}</span>\n                ) : (\n                  emptyState\n                )}\n              </div>\n            </TableCell>\n          </TableRow>\n        )}\n      </TableBody>\n    );\n  }\n\n  return (\n    <TableBody>\n      {scrolls && paddingTop > 0 ? (\n        <tr aria-hidden style={{ height: paddingTop }}>\n          <td colSpan={colSpan} />\n        </tr>\n      ) : null}\n      {renderedRows.map(({ entry, index }) => {\n        const detail = renderRowDetail?.(entry.row) ?? null;\n        const detailId = detail === null ? undefined : `${entry.id}-detail`;\n        const isLastRow = index === rowCount - 1 && !loadingMore;\n        return (\n          <Fragment key={entry.id}>\n            <DataTableBodyRow\n              className={getRowClassName?.(entry.row)}\n              columns={columns}\n              detailId={detailId}\n              entry={entry}\n              expanded={renderRowDetail ? detail !== null : undefined}\n              index={index}\n              isLastRow={isLastRow && detail === null}\n              isSelected={selected.has(entry.id)}\n              onRowClick={\n                !isRowClickable || isRowClickable(entry.row)\n                  ? onRowClick\n                  : undefined\n              }\n              onRowPointerEnter={onRowPointerEnter}\n              onToggleRow={onToggleRow}\n              renderRowContextMenu={renderRowContextMenu}\n              rowHeight={rowHeight}\n              rowKeyboardActivation={rowKeyboardActivation}\n              rowSizing={rowSizing}\n              selectable={selectable}\n            />\n            {detail === null ? null : (\n              <TableRow className=\"hover:bg-transparent\" id={detailId}>\n                <TableCell\n                  className={cn(\n                    \"bg-muted/20 h-auto p-0 whitespace-normal\",\n                    isLastRow ? \"border-b-0\" : TABLE_ROW_BORDER_CLASS\n                  )}\n                  colSpan={colSpan}\n                >\n                  {detail}\n                </TableCell>\n              </TableRow>\n            )}\n          </Fragment>\n        );\n      })}\n      {scrolls && paddingBottom > 0 ? (\n        <tr aria-hidden style={{ height: paddingBottom }}>\n          <td colSpan={colSpan} />\n        </tr>\n      ) : null}\n      {loadingMore ? (\n        <SkeletonRows\n          columns={columns}\n          count={skeletonRows}\n          keyPrefix=\"more\"\n          rowHeight={rowHeight}\n          selectable={selectable}\n        />\n      ) : null}\n    </TableBody>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/table/components/data-table-body.tsx"
    },
    {
      "path": "registry/notra/table/components/data-table-column-group.tsx",
      "content": "import { CHECKBOX_COLUMN_WIDTH } from \"../constants/data-table\";\nimport {\n  colWidthStyle,\n  headerMinWidth,\n  isFrWidth,\n  resolveColumnWidths,\n} from \"../lib/data-table\";\nimport type { TableColumnGroupProps } from \"../types/data-table\";\n\nexport function DataTableColumnGroup<T>({\n  columns,\n  widths,\n  selectable,\n  minColumnWidth,\n}: TableColumnGroupProps<T>) {\n  const resolvedWidths = resolveColumnWidths(\n    columns,\n    selectable ? [CHECKBOX_COLUMN_WIDTH] : []\n  );\n  const hasFlexibleColumn = columns.some(\n    (column) =>\n      widths[column.key] == null && (!column.width || isFrWidth(column.width))\n  );\n\n  return (\n    <colgroup>\n      {selectable ? (\n        <col\n          style={{\n            width: CHECKBOX_COLUMN_WIDTH,\n            minWidth: CHECKBOX_COLUMN_WIDTH,\n          }}\n        />\n      ) : null}\n      {columns.map((column, index) => {\n        const override = widths[column.key];\n        const width = override ? `${override}px` : resolvedWidths[index];\n        const flexible =\n          override == null && (!column.width || isFrWidth(column.width));\n        const minWidth = headerMinWidth(column, minColumnWidth);\n        return (\n          <col\n            key={column.key}\n            style={colWidthStyle(width, flexible, minWidth)}\n          />\n        );\n      })}\n      <col style={hasFlexibleColumn ? { width: 0 } : undefined} />\n    </colgroup>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/table/components/data-table-column-group.tsx"
    },
    {
      "path": "registry/notra/table/components/data-table-header.tsx",
      "content": "\"use client\";\n\nimport {\n  ArrowUp01Icon,\n  InformationCircleIcon,\n} from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\nimport type { ReactElement } from \"react\";\n\nimport { Checkbox } from \"@/components/ui/checkbox\";\n\nimport { TableHead, TableHeader, TableRow } from \"../../table/components/table\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"../../tooltip/components/tooltip\";\nimport { alignFlex, alignText, headerMinWidth } from \"../lib/data-table\";\nimport type { DataTableHeaderProps, TableColumn } from \"../types/data-table\";\nimport { useDataTableLabels } from \"./data-table-labels\";\n\nconst HINT_ICON_PX_SIZE = 13;\n\nfunction HeaderLabel<T>({ column }: { column: TableColumn<T> }) {\n  if (!column.hint) {\n    return <span className=\"whitespace-nowrap\">{column.header}</span>;\n  }\n  return (\n    <span className=\"inline-flex items-center gap-1 whitespace-nowrap\">\n      {column.header}\n      <HugeiconsIcon\n        aria-hidden\n        className=\"shrink-0 opacity-60\"\n        icon={InformationCircleIcon}\n        size={HINT_ICON_PX_SIZE}\n      />\n    </span>\n  );\n}\n\nfunction WithHeaderHint<T>({\n  column,\n  children,\n}: {\n  column: TableColumn<T>;\n  children: ReactElement;\n}) {\n  if (!column.hint) {\n    return children;\n  }\n  return (\n    <Tooltip>\n      <TooltipTrigger render={children} />\n      <TooltipContent side=\"top\">\n        <div className=\"max-w-64 text-xs\">{column.hint}</div>\n      </TooltipContent>\n    </Tooltip>\n  );\n}\n\nexport function DataTableHeader<T>({\n  columns,\n  rowHeight,\n  thRefs,\n  selectable,\n  allSelected,\n  someSelected,\n  onToggleAll,\n  sort,\n  onToggleSort,\n  resizable,\n  onResizeStart,\n  onResizeMove,\n  onResizeEnd,\n  minColumnWidth,\n}: DataTableHeaderProps<T>) {\n  const labels = useDataTableLabels();\n  return (\n    <TableHeader>\n      <TableRow style={{ height: rowHeight }}>\n        {selectable ? (\n          <TableHead className=\"h-auto p-0\">\n            <div className=\"flex items-center justify-center\">\n              <Checkbox\n                className=\"cursor-pointer\"\n                aria-label={labels.selectAllRows}\n                checked={allSelected}\n                indeterminate={!allSelected && someSelected}\n                onCheckedChange={onToggleAll}\n              />\n            </div>\n          </TableHead>\n        ) : null}\n        {columns.map((column) => {\n          const active = sort?.key === column.key;\n          let ariaSort: \"ascending\" | \"descending\" | undefined;\n          if (active) {\n            ariaSort = sort?.direction === \"asc\" ? \"ascending\" : \"descending\";\n          }\n          return (\n            <TableHead\n              aria-sort={ariaSort}\n              className=\"group relative h-auto p-0\"\n              key={column.key}\n              ref={(el) => {\n                thRefs.current[column.key] = el;\n              }}\n              style={{ minWidth: headerMinWidth(column, minColumnWidth) }}\n            >\n              <div\n                className={cn(\"flex items-center\", alignFlex(column.align))}\n                style={{ height: rowHeight }}\n              >\n                {column.sortable ? (\n                  <WithHeaderHint column={column}>\n                    <button\n                      className={cn(\n                        \"hover:text-foreground focus-visible:ring-ring/50 flex h-full flex-1 items-center gap-1 rounded-md px-4 transition-colors outline-none select-none focus-visible:ring-[3px]\",\n                        alignFlex(column.align),\n                        active && \"text-foreground\"\n                      )}\n                      onClick={() => onToggleSort(column.key)}\n                      type=\"button\"\n                    >\n                      {column.align === \"right\" ? null : (\n                        <HeaderLabel column={column} />\n                      )}\n                      <span\n                        aria-hidden\n                        className={cn(\n                          \"duration-fast inline-flex shrink-0 transition-[rotate,opacity] ease-out motion-reduce:transition-none\",\n                          active ? \"opacity-100\" : \"opacity-35\",\n                          active && sort?.direction === \"desc\" && \"rotate-180\"\n                        )}\n                      >\n                        <HugeiconsIcon icon={ArrowUp01Icon} size={14} />\n                      </span>\n                      {column.align === \"right\" ? (\n                        <HeaderLabel column={column} />\n                      ) : null}\n                    </button>\n                  </WithHeaderHint>\n                ) : (\n                  <WithHeaderHint column={column}>\n                    <span\n                      className={cn(\n                        \"flex-1 px-4 whitespace-nowrap\",\n                        alignText(column.align)\n                      )}\n                    >\n                      <HeaderLabel column={column} />\n                    </span>\n                  </WithHeaderHint>\n                )}\n              </div>\n              {resizable ? (\n                <button\n                  aria-label={labels.resizeColumn(\n                    typeof column.header === \"string\"\n                      ? column.header\n                      : column.key\n                  )}\n                  className=\"hover:bg-primary/40 absolute top-0 right-0 h-full w-1.5 cursor-col-resize touch-none bg-transparent transition-colors\"\n                  onPointerDown={(e) => onResizeStart(column.key, e)}\n                  onPointerMove={onResizeMove}\n                  onPointerUp={onResizeEnd}\n                  tabIndex={-1}\n                  type=\"button\"\n                />\n              ) : null}\n            </TableHead>\n          );\n        })}\n        <TableHead aria-hidden className=\"h-auto p-0\" />\n      </TableRow>\n    </TableHeader>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/table/components/data-table-header.tsx"
    },
    {
      "path": "registry/notra/table/components/data-table-labels.tsx",
      "content": "\"use client\";\n\nimport { createContext, type ReactNode, useContext, useMemo } from \"react\";\n\nimport { DATA_TABLE_LABELS } from \"../constants/data-table\";\nimport type { DataTableLabels } from \"../types/data-table\";\n\nconst DataTableLabelsContext =\n  createContext<DataTableLabels>(DATA_TABLE_LABELS);\n\nexport function DataTableLabelsProvider({\n  labels,\n  children,\n}: {\n  labels?: Partial<DataTableLabels>;\n  children: ReactNode;\n}) {\n  const parent = useContext(DataTableLabelsContext);\n  const value = useMemo(() => ({ ...parent, ...labels }), [parent, labels]);\n  return (\n    <DataTableLabelsContext.Provider value={value}>\n      {children}\n    </DataTableLabelsContext.Provider>\n  );\n}\n\nexport function useDataTableLabels(): DataTableLabels {\n  return useContext(DataTableLabelsContext);\n}\n",
      "type": "registry:component",
      "target": "components/table/components/data-table-labels.tsx"
    },
    {
      "path": "registry/notra/table/components/data-table-pager.tsx",
      "content": "\"use client\";\n\nimport type { DataTablePagerProps } from \"../types/data-table\";\nimport { TablePagination } from \"./data-table-pagination\";\n\n/** The footer pager every paged table shares, whatever the paging source. */\nexport function DataTablePager({\n  pagination,\n  rowCount,\n  onClientPageSizeChange,\n}: DataTablePagerProps) {\n  const {\n    page,\n    pageSize,\n    itemLabel,\n    formatRange,\n    onPageChange,\n    pageSizeOptions,\n    pageSizeSelector = true,\n  } = pagination;\n  const changePageSize = pageSizeSelector\n    ? (pagination.onPageSizeChange ?? onClientPageSizeChange)\n    : undefined;\n  const onPageSizeChange = changePageSize\n    ? (next: number) => {\n        changePageSize(next);\n        onPageChange(1);\n      }\n    : undefined;\n\n  if (pagination.mode === \"cursor\") {\n    if (rowCount === 0 && page <= 1) {\n      return null;\n    }\n    return (\n      <TablePagination\n        formatRange={formatRange}\n        hasNextPage={pagination.hasNextPage}\n        itemLabel={itemLabel}\n        onPageSizeChange={onPageSizeChange}\n        page={page}\n        pageRowCount={rowCount}\n        pageSizeOptions={pageSizeOptions}\n        pageSize={pageSize}\n        setPage={(next) => onPageChange(Math.max(1, next))}\n      />\n    );\n  }\n\n  const totalItems =\n    pagination.mode === \"server\" ? pagination.totalItems : rowCount;\n  if (totalItems === 0) {\n    return null;\n  }\n  const pageCount = Math.max(1, Math.ceil(totalItems / pageSize));\n  const currentPage = Math.min(Math.max(1, page), pageCount);\n  return (\n    <TablePagination\n      formatRange={formatRange}\n      itemLabel={itemLabel}\n      onPageSizeChange={onPageSizeChange}\n      page={currentPage}\n      pageCount={pageCount}\n      pageRowCount={Math.min(\n        pageSize,\n        totalItems - (currentPage - 1) * pageSize\n      )}\n      pageSize={pageSize}\n      pageSizeOptions={pageSizeOptions}\n      setPage={(next) => onPageChange(Math.min(Math.max(1, next), pageCount))}\n      totalItems={totalItems}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/table/components/data-table-pager.tsx"
    },
    {
      "path": "registry/notra/table/components/data-table-pagination.tsx",
      "content": "\"use client\";\n\nimport { ArrowLeft01Icon, ArrowRight01Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\n\nimport { Button } from \"../../button/components/button\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"../../select/components/select\";\nimport { TABLE_PAGE_SIZE_OPTIONS } from \"../constants/data-table\";\nimport type { TablePaginationRange } from \"../types/data-table\";\nimport { useDataTableLabels } from \"./data-table-labels\";\n\ninterface TablePaginationProps {\n  page: number;\n  /** Omit when only \"is there a next page\" is known (cursor paging). */\n  pageCount?: number;\n  pageSize: number;\n  /** Omit when only \"is there a next page\" is known (cursor paging). */\n  totalItems?: number;\n  /** Required without `pageCount`: whether a page follows this one. */\n  hasNextPage?: boolean;\n  pageRowCount: number;\n  setPage: (page: number) => void;\n  itemLabel?: string;\n  className?: string;\n  formatRange?: (range: TablePaginationRange) => string;\n  /** Shows a page-size select in place of the range text. */\n  onPageSizeChange?: (pageSize: number) => void;\n  pageSizeOptions?: readonly number[];\n}\n\nfunction pageSizeChoices(\n  options: readonly number[],\n  pageSize: number\n): number[] {\n  return [...new Set([...options, pageSize])].sort((a, b) => a - b);\n}\n\nfunction pageRange(\n  page: number,\n  pageSize: number,\n  pageRowCount: number,\n  totalItems: number | undefined\n): TablePaginationRange {\n  const offset = (page - 1) * pageSize;\n  if (totalItems === undefined) {\n    return {\n      start: pageRowCount === 0 ? 0 : offset + 1,\n      end: offset + pageRowCount,\n    };\n  }\n  return {\n    start: totalItems === 0 || pageRowCount === 0 ? 0 : offset + 1,\n    end: Math.min(totalItems, page * pageSize),\n    total: totalItems,\n  };\n}\n\nfunction RangeText({\n  range,\n  itemLabel,\n  formatRange,\n}: {\n  range: TablePaginationRange;\n  itemLabel?: string;\n  formatRange?: (range: TablePaginationRange) => string;\n}) {\n  const labels = useDataTableLabels();\n  const locale = labels.locale ?? \"en-US\";\n  let text = formatRange?.(range);\n  if (text === undefined) {\n    const start = range.start.toLocaleString(locale);\n    const end = range.end.toLocaleString(locale);\n    const base =\n      range.total === undefined\n        ? labels.paginationRangeOpen(start, end)\n        : labels.paginationRange(\n            start,\n            end,\n            range.total.toLocaleString(locale)\n          );\n    text = itemLabel ? `${base} ${itemLabel}` : base;\n  }\n  return <span className=\"min-w-0 truncate tabular-nums\">{text}</span>;\n}\n\nfunction PageSizeSelect({\n  pageSize,\n  options,\n  onPageSizeChange,\n}: {\n  pageSize: number;\n  options: readonly number[];\n  onPageSizeChange: (pageSize: number) => void;\n}) {\n  const labels = useDataTableLabels();\n  const locale = labels.locale ?? \"en-US\";\n  return (\n    <Select\n      onValueChange={(value: string | null) => {\n        if (value) {\n          onPageSizeChange(Number(value));\n        }\n      }}\n      value={String(pageSize)}\n    >\n      <SelectTrigger\n        aria-label={labels.rowsPerPage}\n        className=\"-ml-2.5 text-xs\"\n        size=\"sm\"\n        variant=\"ghost\"\n      >\n        <SelectValue>\n          {(value: string) =>\n            labels.showRows(Number(value).toLocaleString(locale))\n          }\n        </SelectValue>\n      </SelectTrigger>\n      <SelectContent>\n        {pageSizeChoices(options, pageSize).map((size) => (\n          <SelectItem key={size} value={String(size)}>\n            {labels.showRows(size.toLocaleString(locale))}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n}\n\nfunction PageControls({\n  page,\n  pageCount,\n  isFirst,\n  isLast,\n  setPage,\n}: {\n  page: number;\n  pageCount?: number;\n  isFirst: boolean;\n  isLast: boolean;\n  setPage: (page: number) => void;\n}) {\n  const labels = useDataTableLabels();\n  const locale = labels.locale ?? \"en-US\";\n  const pageText = page.toLocaleString(locale);\n  return (\n    <nav\n      aria-label={labels.pagination}\n      className=\"ml-auto flex items-center gap-3\"\n    >\n      <span aria-live=\"polite\" className=\"tabular-nums\">\n        {pageCount === undefined\n          ? labels.pageNumber(pageText)\n          : labels.pageOf(pageText, pageCount.toLocaleString(locale))}\n      </span>\n      <div className=\"flex items-center gap-1\">\n        <Button\n          aria-label={labels.goToPreviousPage}\n          disabled={isFirst}\n          onClick={() => setPage(page - 1)}\n          size=\"icon-sm\"\n          variant=\"outline\"\n        >\n          <HugeiconsIcon icon={ArrowLeft01Icon} strokeWidth={2} />\n        </Button>\n        <Button\n          aria-label={labels.goToNextPage}\n          disabled={isLast}\n          onClick={() => setPage(page + 1)}\n          size=\"icon-sm\"\n          variant=\"outline\"\n        >\n          <HugeiconsIcon icon={ArrowRight01Icon} strokeWidth={2} />\n        </Button>\n      </div>\n    </nav>\n  );\n}\n\n/**\n * Footer pager: the page-size select (or the visible range) on the left,\n * \"Page 2 of 9\" and prev/next on the right. One shape for known and unknown\n * totals, so every table pages the same way.\n */\nexport function TablePagination({\n  page,\n  pageCount,\n  pageSize,\n  totalItems,\n  hasNextPage = false,\n  pageRowCount,\n  setPage,\n  itemLabel,\n  className,\n  formatRange,\n  onPageSizeChange,\n  pageSizeOptions = TABLE_PAGE_SIZE_OPTIONS,\n}: TablePaginationProps) {\n  const range = pageRange(page, pageSize, pageRowCount, totalItems);\n  const isFirst = page <= 1;\n  const isLast = pageCount === undefined ? !hasNextPage : page >= pageCount;\n  const hasPages = !(isFirst && isLast);\n  const smallestSize = Math.min(...pageSizeOptions, pageSize);\n  const hasMoreThanSmallest =\n    range.total === undefined\n      ? hasPages || pageRowCount > smallestSize\n      : range.total > smallestSize;\n\n  return (\n    <div\n      className={cn(\n        \"text-muted-foreground flex min-h-11 flex-wrap items-center justify-between gap-x-3 gap-y-1 px-4 py-1.5 text-xs\",\n        className\n      )}\n    >\n      {onPageSizeChange && hasMoreThanSmallest ? (\n        <PageSizeSelect\n          onPageSizeChange={onPageSizeChange}\n          options={pageSizeOptions}\n          pageSize={pageSize}\n        />\n      ) : (\n        <RangeText\n          formatRange={formatRange}\n          itemLabel={itemLabel}\n          range={range}\n        />\n      )}\n      {hasPages ? (\n        <PageControls\n          isFirst={isFirst}\n          isLast={isLast}\n          page={page}\n          pageCount={pageCount}\n          setPage={setPage}\n        />\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/table/components/data-table-pagination.tsx"
    },
    {
      "path": "registry/notra/table/components/data-table-root.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport { useRef } from \"react\";\n\nimport {\n  CHECKBOX_COLUMN_WIDTH,\n  DATA_TABLE_HEIGHT,\n  DATA_TABLE_OVERSCAN,\n  DATA_TABLE_HEADER_HEIGHT,\n  DATA_TABLE_ROW_HEIGHT,\n  DATA_TABLE_SKELETON_ROWS,\n  DEFAULT_MIN_COLUMN_WIDTH,\n  TABLE_FRAME_INSET,\n} from \"../constants/data-table\";\nimport { useCollapsibleColumns } from \"../hooks/use-collapsible-columns\";\nimport { useColumnResize } from \"../hooks/use-column-resize\";\nimport { useColumnSort } from \"../hooks/use-column-sort\";\nimport { useRowSelection } from \"../hooks/use-row-selection\";\nimport { useTablePaging } from \"../hooks/use-table-paging\";\nimport { useTableViewport } from \"../hooks/use-table-viewport\";\nimport {\n  pinRowsFirst,\n  tableLoadingOverlay,\n  tableLayout,\n} from \"../lib/data-table\";\nimport type { DataTableRootProps, HeaderCellRefs } from \"../types/data-table\";\nimport { DataTableBody } from \"./data-table-body\";\nimport { DataTableColumnGroup } from \"./data-table-column-group\";\nimport { DataTableHeader } from \"./data-table-header\";\nimport {\n  DataTableLabelsProvider,\n  useDataTableLabels,\n} from \"./data-table-labels\";\nimport { DataTablePager } from \"./data-table-pager\";\nimport {\n  TableBodySurface,\n  TableFooterSurface,\n  TableFrame,\n  TableHeaderSurface,\n  TableScrollFade,\n} from \"./data-table-surfaces\";\n\nexport function DataTableRoot<T>(props: DataTableRootProps<T>) {\n  return (\n    <DataTableLabelsProvider labels={props.labels}>\n      <DataTableRootInner {...props} />\n    </DataTableLabelsProvider>\n  );\n}\n\nfunction DataTableRootInner<T>({\n  data,\n  columns,\n  getRowId,\n  selectable = false,\n  selectedRowIds,\n  defaultSelectedRowIds,\n  onSelectionChange,\n  sort: sortProp,\n  defaultSort = null,\n  onSortChange,\n  manualSort = false,\n  resizable = false,\n  minColumnWidth = DEFAULT_MIN_COLUMN_WIDTH,\n  onColumnResize,\n  rowHeight = DATA_TABLE_ROW_HEIGHT,\n  headerHeight = DATA_TABLE_HEADER_HEIGHT,\n  rowSizing = \"fixed\",\n  height: heightProp,\n  minHeight,\n  autoHeight = false,\n  overscan = DATA_TABLE_OVERSCAN,\n  onEndReached,\n  loading = false,\n  loadingMore: loadingMoreProp,\n  skeletonRows = DATA_TABLE_SKELETON_ROWS,\n  emptyState: emptyStateProp,\n  onRowClick,\n  rowKeyboardActivation = true,\n  isRowClickable,\n  getRowClassName,\n  renderRowContextMenu,\n  renderRowDetail,\n  onRowPointerEnter,\n  isRowPinned,\n  toolbar,\n  footer,\n  pagination,\n  visibleRowCount,\n  flushTop = false,\n  flushBottom = false,\n  scrollFade = true,\n  className,\n}: DataTableRootProps<T>) {\n  const labels = useDataTableLabels();\n  const emptyState = emptyStateProp ?? labels.noData;\n  const thRefs: HeaderCellRefs = useRef<\n    Record<string, HTMLTableCellElement | null>\n  >({});\n  const rows = data.map((row, index) => ({\n    row,\n    id: getRowId ? getRowId(row, index) : String(index),\n  }));\n  const { containerRef, visibleColumns } = useCollapsibleColumns(columns, {\n    minColumnWidth,\n    extraFixedWidths: selectable\n      ? [TABLE_FRAME_INSET, CHECKBOX_COLUMN_WIDTH]\n      : [TABLE_FRAME_INSET],\n  });\n  const { sort, sortedRows, toggleSort } = useColumnSort({\n    rows,\n    columns,\n    sort: sortProp,\n    defaultSort,\n    onSortChange,\n    manualSort,\n  });\n  const { widths, startResize, moveResize, endResize } = useColumnResize({\n    orderedColumns: visibleColumns,\n    thRefs,\n    minColumnWidth,\n    onColumnResize,\n  });\n  const { selected, allSelected, someSelected, toggleAll, toggleRow } =\n    useRowSelection({\n      sortedRows,\n      selectedRowIds,\n      defaultSelectedRowIds,\n      onSelectionChange,\n    });\n  const displayRows = pinRowsFirst(sortedRows, isRowPinned);\n  const paging = useTablePaging(displayRows, pagination, visibleRowCount);\n  const { pagedRows } = paging;\n  const footerContent =\n    footer || pagination ? (\n      <>\n        {footer}\n        {pagination ? (\n          <DataTablePager\n            onClientPageSizeChange={paging.onClientPageSizeChange}\n            pagination={paging.pagination ?? pagination}\n            rowCount={displayRows.length}\n          />\n        ) : null}\n      </>\n    ) : undefined;\n\n  const height =\n    heightProp ??\n    (pagination\n      ? Math.max(pagedRows.length, 1) * rowHeight + headerHeight\n      : DATA_TABLE_HEIGHT);\n  const {\n    headerScrollRef,\n    scrollRef,\n    headerStyle,\n    bodyStyle,\n    overflowClass,\n    handleScroll,\n    renderedRows,\n    bodyHeight,\n    scrolls,\n    paddingTop,\n    paddingBottom,\n    atEnd,\n    atStart,\n    overflowsX,\n  } = useTableViewport({\n    rows: pagedRows,\n    rowHeight,\n    headerHeight,\n    rowSizing,\n    height,\n    minHeight,\n    autoHeight,\n    overscan,\n    loading,\n    onEndReached,\n  });\n  const columnGroup = (\n    <DataTableColumnGroup\n      columns={visibleColumns}\n      minColumnWidth={minColumnWidth}\n      selectable={selectable}\n      widths={widths}\n    />\n  );\n  const isEmpty = pagedRows.length === 0 && !loading;\n  const { loadingMore, dimRows, loadingState } = tableLoadingOverlay(\n    loading,\n    pagedRows.length,\n    loadingMoreProp,\n    Boolean(onEndReached)\n  );\n  const layout = tableLayout(\n    visibleColumns,\n    widths,\n    minColumnWidth,\n    selectable ? [CHECKBOX_COLUMN_WIDTH] : []\n  );\n  const tableClassName = cn(\n    \"border-separate border-spacing-0 text-sm tabular-nums\",\n    layout.className\n  );\n  const tableStyle = layout.style;\n\n  return (\n    <div\n      aria-busy={loading}\n      className={cn(\"w-full min-w-0 text-sm\", className)}\n      data-slot=\"data-table\"\n      ref={containerRef}\n    >\n      <TableFrame flushBottom={flushBottom} flushTop={flushTop}>\n        <TableHeaderSurface flushTop={flushTop} toolbar={toolbar}>\n          <div\n            className=\"overflow-hidden border-x border-transparent\"\n            ref={headerScrollRef}\n            style={headerStyle}\n          >\n            <table className={tableClassName} style={tableStyle}>\n              {columnGroup}\n              <DataTableHeader\n                allSelected={allSelected}\n                columns={visibleColumns}\n                minColumnWidth={minColumnWidth}\n                onResizeEnd={endResize}\n                onResizeMove={moveResize}\n                onResizeStart={startResize}\n                onToggleAll={toggleAll}\n                onToggleSort={toggleSort}\n                resizable={resizable}\n                rowHeight={headerHeight}\n                selectable={selectable}\n                someSelected={someSelected}\n                sort={sort}\n                thRefs={thRefs}\n              />\n            </table>\n          </div>\n        </TableHeaderSurface>\n        <TableBodySurface\n          cornerScrollbar={!overflowsX}\n          dimRows={dimRows}\n          isEmpty={isEmpty}\n          loadingState={loadingState}\n          onScroll={handleScroll}\n          overflowClass={overflowClass}\n          scrollRef={scrollRef}\n          style={bodyStyle}\n        >\n          <TableScrollFade\n            edge=\"top\"\n            hidden={atStart}\n            scrollFade={scrollFade}\n          />\n          <table\n            className={cn(tableClassName, isEmpty && \"w-full table-fixed\")}\n            style={isEmpty ? undefined : tableStyle}\n          >\n            {isEmpty ? null : columnGroup}\n            <DataTableBody\n              bodyHeight={bodyHeight}\n              columns={visibleColumns}\n              emptyState={emptyState}\n              getRowClassName={getRowClassName}\n              isRowClickable={isRowClickable}\n              loading={loading}\n              loadingMore={loadingMore}\n              onRowClick={onRowClick}\n              onRowPointerEnter={onRowPointerEnter}\n              onToggleRow={toggleRow}\n              paddingBottom={paddingBottom}\n              paddingTop={paddingTop}\n              renderRowContextMenu={renderRowContextMenu}\n              renderRowDetail={renderRowDetail}\n              renderedRows={renderedRows}\n              rowCount={pagedRows.length}\n              rowHeight={rowHeight}\n              rowKeyboardActivation={rowKeyboardActivation}\n              rowSizing={rowSizing}\n              scrolls={scrolls}\n              selectable={selectable}\n              selected={selected}\n              skeletonRows={skeletonRows}\n            />\n          </table>\n          <TableScrollFade\n            edge=\"bottom\"\n            hidden={atEnd}\n            scrollFade={scrollFade}\n          />\n        </TableBodySurface>\n        <TableFooterSurface footer={footerContent} />\n      </TableFrame>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/table/components/data-table-root.tsx"
    },
    {
      "path": "registry/notra/table/components/data-table-corner-scrollbar.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nimport {\n  CORNER_SCROLLBAR_GAP_PX,\n  CORNER_SCROLLBAR_IDLE_MS,\n  CORNER_SCROLLBAR_MIN_THUMB_PX,\n  DATA_TABLE_ROW_HEIGHT,\n  TABLE_BODY_RADIUS_PX,\n} from \"../constants/data-table\";\nimport type { TableCornerScrollbarProps } from \"../types/data-table\";\nimport { useDataTableLabels } from \"./data-table-labels\";\n\n/**\n * Straight scrollbar thumb along the card's right edge. While it is visible\n * and runs into the top or bottom corner, it flags that corner on the\n * wrapper (`data-corner-start` / `data-corner-end`) so the card can tighten\n * its radius around the thumb instead of clipping it. Size and offset come\n * from `CORNER_SCROLLBAR_VARS` on the table frame.\n */\nexport function TableCornerScrollbar({\n  scrollId,\n  scrollRef,\n  wrapperRef,\n}: TableCornerScrollbarProps) {\n  const labels = useDataTableLabels();\n  const thumbRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const element = scrollRef.current;\n    const wrapper = wrapperRef.current;\n    const thumb = thumbRef.current;\n    if (!(element && wrapper && thumb)) {\n      return;\n    }\n    let travel = 0;\n    let idleTimer: ReturnType<typeof setTimeout> | undefined;\n    let hovered = false;\n    let scrolling = false;\n    let dragging = false;\n\n    const update = () => {\n      const scrollable = Math.max(\n        0,\n        element.scrollHeight - element.clientHeight\n      );\n      const position = Math.min(scrollable, Math.max(0, element.scrollTop));\n      const visible =\n        scrollable > 1 &&\n        (hovered ||\n          scrolling ||\n          dragging ||\n          thumb === thumb.ownerDocument.activeElement);\n      thumb.hidden = scrollable <= 1;\n      thumb.tabIndex = scrollable > 1 ? 0 : -1;\n      thumb.ariaValueMax = String(scrollable);\n      thumb.ariaValueNow = String(position);\n      wrapper.dataset.thumb = visible ? \"visible\" : \"hidden\";\n      if (scrollable <= 1) {\n        wrapper.dataset.cornerStart = \"false\";\n        wrapper.dataset.cornerEnd = \"false\";\n        return;\n      }\n      const track = element.offsetHeight - CORNER_SCROLLBAR_GAP_PX * 2;\n      const length = Math.min(\n        track,\n        Math.max(\n          CORNER_SCROLLBAR_MIN_THUMB_PX,\n          (element.clientHeight / element.scrollHeight) * track\n        )\n      );\n      travel = track - length;\n      const progress = position / scrollable;\n      const top = CORNER_SCROLLBAR_GAP_PX + progress * travel;\n      thumb.style.height = `${length}px`;\n      thumb.style.transform = `translateY(${top}px)`;\n      wrapper.dataset.cornerStart = String(\n        visible && top < TABLE_BODY_RADIUS_PX\n      );\n      wrapper.dataset.cornerEnd = String(\n        visible && top + length > element.offsetHeight - TABLE_BODY_RADIUS_PX\n      );\n    };\n\n    const handleScroll = () => {\n      scrolling = true;\n      update();\n      clearTimeout(idleTimer);\n      idleTimer = setTimeout(() => {\n        scrolling = false;\n        update();\n      }, CORNER_SCROLLBAR_IDLE_MS);\n    };\n    const handlePointerEnter = () => {\n      hovered = true;\n      update();\n    };\n    const handlePointerLeave = () => {\n      hovered = false;\n      update();\n    };\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.altKey || event.ctrlKey || event.metaKey) {\n        return;\n      }\n      const scrollable = Math.max(\n        0,\n        element.scrollHeight - element.clientHeight\n      );\n      let position = element.scrollTop;\n      switch (event.key) {\n        case \"ArrowUp\":\n          position -= DATA_TABLE_ROW_HEIGHT;\n          break;\n        case \"ArrowDown\":\n          position += DATA_TABLE_ROW_HEIGHT;\n          break;\n        case \"PageUp\":\n          position -= element.clientHeight;\n          break;\n        case \"PageDown\":\n          position += element.clientHeight;\n          break;\n        case \"Home\":\n          position = 0;\n          break;\n        case \"End\":\n          position = scrollable;\n          break;\n        default:\n          return;\n      }\n      event.preventDefault();\n      element.scrollTop = Math.min(scrollable, Math.max(0, position));\n      update();\n    };\n\n    let dragStartY = 0;\n    let dragStartScroll = 0;\n    const handlePointerDown = (event: PointerEvent) => {\n      event.preventDefault();\n      thumb.setPointerCapture(event.pointerId);\n      dragStartY = event.clientY;\n      dragStartScroll = element.scrollTop;\n      dragging = true;\n      update();\n    };\n    const handlePointerMove = (event: PointerEvent) => {\n      if (!dragging || travel <= 0) {\n        return;\n      }\n      element.scrollTop =\n        dragStartScroll +\n        ((event.clientY - dragStartY) / travel) *\n          (element.scrollHeight - element.clientHeight);\n    };\n    const handlePointerUp = () => {\n      dragging = false;\n      update();\n    };\n\n    update();\n    element.addEventListener(\"scroll\", handleScroll, { passive: true });\n    wrapper.addEventListener(\"pointerenter\", handlePointerEnter);\n    wrapper.addEventListener(\"pointerleave\", handlePointerLeave);\n    thumb.addEventListener(\"focus\", update);\n    thumb.addEventListener(\"blur\", update);\n    thumb.addEventListener(\"keydown\", handleKeyDown);\n    thumb.addEventListener(\"pointerdown\", handlePointerDown);\n    thumb.addEventListener(\"pointermove\", handlePointerMove);\n    thumb.addEventListener(\"pointerup\", handlePointerUp);\n    thumb.addEventListener(\"pointercancel\", handlePointerUp);\n    const observer = new ResizeObserver(update);\n    observer.observe(element);\n    const table = element.querySelector(\"table\");\n    if (table) {\n      observer.observe(table);\n    }\n    return () => {\n      clearTimeout(idleTimer);\n      observer.disconnect();\n      element.removeEventListener(\"scroll\", handleScroll);\n      wrapper.removeEventListener(\"pointerenter\", handlePointerEnter);\n      wrapper.removeEventListener(\"pointerleave\", handlePointerLeave);\n      thumb.removeEventListener(\"focus\", update);\n      thumb.removeEventListener(\"blur\", update);\n      thumb.removeEventListener(\"keydown\", handleKeyDown);\n      thumb.removeEventListener(\"pointerdown\", handlePointerDown);\n      thumb.removeEventListener(\"pointermove\", handlePointerMove);\n      thumb.removeEventListener(\"pointerup\", handlePointerUp);\n      thumb.removeEventListener(\"pointercancel\", handlePointerUp);\n    };\n  }, [scrollRef, wrapperRef]);\n\n  return (\n    <div\n      aria-controls={scrollId}\n      aria-label={labels.table}\n      aria-orientation=\"vertical\"\n      aria-valuemax={0}\n      aria-valuemin={0}\n      aria-valuenow={0}\n      className=\"focus-visible:bg-foreground focus-visible:outline-foreground pointer-events-none absolute top-0 right-(--corner-scrollbar-gap) z-20 w-(--corner-scrollbar-width) rounded-full bg-[color-mix(in_oklab,var(--foreground)_22%,transparent)] opacity-0 transition-[opacity,background-color] duration-200 group-data-[thumb=visible]/table-body:pointer-events-auto group-data-[thumb=visible]/table-body:opacity-100 hover:bg-[color-mix(in_oklab,var(--foreground)_35%,transparent)] focus-visible:outline-2 focus-visible:outline-offset-2 active:bg-[color-mix(in_oklab,var(--foreground)_45%,transparent)] motion-reduce:transition-none forced-colors:focus-visible:outline-[Highlight]\"\n      hidden\n      ref={thumbRef}\n      role=\"scrollbar\"\n      tabIndex={-1}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/table/components/data-table-corner-scrollbar.tsx"
    },
    {
      "path": "registry/notra/table/components/data-table-surfaces.tsx",
      "content": "import { cn } from \"cn\";\nimport { useId, useRef } from \"react\";\n\nimport {\n  CORNER_SCROLLBAR_VARS,\n  TABLE_BODY_CLASS,\n  TABLE_FRAME_CLASS,\n} from \"../constants/data-table\";\nimport type {\n  TableBodySurfaceProps,\n  TableFrameProps,\n  TableFooterSurfaceProps,\n  TableHeaderSurfaceProps,\n  TableScrollFadeProps,\n} from \"../types/data-table\";\nimport { TableCornerScrollbar } from \"./data-table-corner-scrollbar\";\n\n/**\n * Grey shell that wraps header, body and footer with a 2px rim. A flush edge\n * joins the shell of the table next to it: the border and radius go, the rim\n * stays, so stacked tables read as one tray with a card per section.\n */\nexport function TableFrame({\n  flushTop,\n  flushBottom,\n  children,\n}: TableFrameProps) {\n  return (\n    <div\n      className={cn(\n        TABLE_FRAME_CLASS,\n        flushTop && \"rounded-t-none border-t-0\",\n        flushBottom && \"rounded-b-none border-b-0\",\n        !flushBottom &&\n          \"transition-[border-radius] duration-200 ease-out has-[>[data-corner-end=true]:last-child]:rounded-br-(--corner-shell-radius) motion-reduce:transition-none\"\n      )}\n      style={CORNER_SCROLLBAR_VARS}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function TableHeaderSurface({\n  toolbar,\n  flushTop,\n  children,\n}: TableHeaderSurfaceProps) {\n  return (\n    <div\n      className={cn(\n        \"overflow-hidden rounded-t-[14px] pb-5\",\n        flushTop && \"rounded-t-none\"\n      )}\n    >\n      {toolbar ? <div className={TABLE_BODY_CLASS}>{toolbar}</div> : null}\n      {children}\n    </div>\n  );\n}\n\n/**\n * Soft fade over the edge where more rows are hidden: at the bottom until the\n * reader reaches the end, at the top once they scroll away from the start.\n */\nexport function TableScrollFade({\n  scrollFade,\n  edge,\n  hidden,\n}: TableScrollFadeProps) {\n  if (!scrollFade) {\n    return null;\n  }\n  return (\n    <div\n      aria-hidden=\"true\"\n      className={cn(\n        \"from-table-card pointer-events-none sticky left-0 z-10 h-8 to-transparent transition-opacity duration-200 motion-reduce:transition-none\",\n        edge === \"top\"\n          ? \"top-0 -mb-8 bg-linear-to-b\"\n          : \"bottom-0 -mt-8 bg-linear-to-t\",\n        hidden ? \"opacity-0\" : \"opacity-100\"\n      )}\n    />\n  );\n}\n\nexport function TableBodySurface({\n  isEmpty,\n  overflowClass,\n  dimRows,\n  loadingState,\n  onScroll,\n  scrollRef,\n  style,\n  cornerScrollbar,\n  children,\n}: TableBodySurfaceProps) {\n  const scrollId = useId();\n  const wrapperRef = useRef<HTMLDivElement>(null);\n  const body = (\n    <div\n      className={cn(\n        \"relative box-content outline-none\",\n        cornerScrollbar\n          ? \"scrollbar-none transition-[border-radius] duration-200 ease-out group-data-[corner-end=true]/table-body:rounded-br-(--corner-card-radius) group-data-[corner-start=true]/table-body:rounded-tr-(--corner-card-radius) motion-reduce:transition-none\"\n          : \"scrollbar-floating -mt-5\",\n        TABLE_BODY_CLASS,\n        isEmpty ? \"overflow-hidden\" : overflowClass,\n        dimRows &&\n          \"pointer-events-none opacity-60 transition-opacity duration-200 motion-reduce:transition-none\"\n      )}\n      data-loading={loadingState}\n      id={scrollId}\n      inert={dimRows ? true : undefined}\n      onScroll={onScroll}\n      ref={scrollRef}\n      style={style}\n    >\n      {children}\n    </div>\n  );\n  if (!cornerScrollbar) {\n    return body;\n  }\n  return (\n    <div\n      className=\"group/table-body relative -mt-5\"\n      inert={dimRows ? true : undefined}\n      ref={wrapperRef}\n    >\n      {body}\n      <TableCornerScrollbar\n        scrollId={scrollId}\n        scrollRef={scrollRef}\n        wrapperRef={wrapperRef}\n      />\n    </div>\n  );\n}\n\nexport function TableFooterSurface({ footer }: TableFooterSurfaceProps) {\n  if (!footer) {\n    return null;\n  }\n  return <div className=\"-mt-5 rounded-b-[14px] pt-5\">{footer}</div>;\n}\n",
      "type": "registry:component",
      "target": "components/table/components/data-table-surfaces.tsx"
    },
    {
      "path": "registry/notra/table/components/data-table.tsx",
      "content": "\"use client\";\n\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nimport {\n  TABLE_SKELETON_COLUMN_WIDTHS,\n  TABLE_SKELETON_ROW_COUNT,\n  TABLE_SKELETON_ROW_HEIGHT,\n} from \"../constants/data-table\";\nimport type {\n  DataTablePagination,\n  DataTableProps,\n  DataTableSkeletonProps,\n  InfiniteDataTableProps,\n} from \"../types/data-table\";\nimport { DataTableRoot } from \"./data-table-root\";\n\nexport type {\n  DataTablePagination,\n  DataTableProps,\n  InfiniteDataTableProps,\n  SortState,\n  TableColumn,\n} from \"../types/data-table\";\n\n/** The app's table: sortable, selectable, paged or bounded with a scrolling body. */\nfunction DataTable<T>(props: DataTableProps<T>) {\n  return <DataTableRoot {...props} />;\n}\n\n/** A fixed-height, virtualized table that asks for the next page near the bottom. */\nfunction InfiniteDataTable<T>(props: InfiniteDataTableProps<T>) {\n  return <DataTableRoot {...props} rowSizing=\"fixed\" />;\n}\n\n/** Loading placeholder with the same chrome as a real table. */\nfunction DataTableSkeleton({\n  columnWidths = TABLE_SKELETON_COLUMN_WIDTHS,\n  rows = TABLE_SKELETON_ROW_COUNT,\n  rowHeight = TABLE_SKELETON_ROW_HEIGHT,\n  height,\n  selectable = false,\n  toolbar,\n  className,\n}: DataTableSkeletonProps) {\n  return (\n    <DataTableRoot\n      className={className}\n      columns={columnWidths.map((column, index) => ({\n        ...(typeof column === \"string\" ? { width: column } : column),\n        key: `skeleton-column-${String(index)}`,\n        header: <Skeleton className=\"h-3.5 w-16\" />,\n      }))}\n      data={[]}\n      height={height ?? (rows + 1) * rowHeight}\n      loading\n      rowHeight={rowHeight}\n      scrollFade={false}\n      selectable={selectable}\n      toolbar={toolbar}\n    />\n  );\n}\n\nexport { DataTable, DataTableSkeleton, InfiniteDataTable };\n",
      "type": "registry:component",
      "target": "components/table/components/data-table.tsx"
    },
    {
      "path": "registry/notra/table/constants/data-table.ts",
      "content": "import type { CSSProperties } from \"react\";\n\nimport type { DataTableLabels } from \"../types/data-table\";\n\n/** Grey shell around every table: 1px outer border plus a 2px rim on all sides. */\nexport const TABLE_FRAME_CLASS =\n  \"rounded-2xl border border-table-shell-border bg-table-shell p-0.5\";\n\n/** The lifted white card inside the shell that holds the rows. */\nexport const TABLE_BODY_CLASS =\n  \"rounded-[14px] border border-table-card-border bg-table-card shadow-table-card\";\n\n/**\n * Dualtone chrome for a single `<table>`: the header sits on the grey shell,\n * the body cells form the lifted white card. `border-separate` is required\n * because row borders and radii only render on cells in that model.\n */\nexport const TABLE_CARD_CELLS_CLASS = [\n  \"[&>tbody>tr>td]:bg-table-card [&>tbody>tr>td]:border-table-row-border [&>tbody>tr>td]:border-b [&>tbody>tr>td]:transition-colors\",\n  \"[&>tbody>tr:hover>td]:bg-[color-mix(in_oklab,var(--muted)_50%,var(--table-card))]\",\n  \"[&>tbody>tr:first-child>td]:border-t-table-card-border [&>tbody>tr:first-child>td]:border-t\",\n  \"[&>tbody>tr:first-child>td]:shadow-[inset_0_1px_0_hsl(0_0%_100%/0.05)]\",\n  \"[&>tbody>tr:last-child>td]:border-b-table-card-border\",\n  \"[&>tbody>tr>td:first-child]:border-l-table-card-border [&>tbody>tr>td:first-child]:border-l\",\n  \"[&>tbody>tr>td:last-child]:border-r-table-card-border [&>tbody>tr>td:last-child]:border-r\",\n  \"[&>tbody>tr:first-child>td:first-child]:rounded-tl-[14px] [&>tbody>tr:first-child>td:last-child]:rounded-tr-[14px]\",\n  \"[&>tbody>tr:last-child>td:first-child]:rounded-bl-[14px] [&>tbody>tr:last-child>td:last-child]:rounded-br-[14px]\",\n].join(\" \");\n\n/** Radius of the white card, so header/footer bands can match it. */\nexport const TABLE_INNER_RADIUS_CLASS = \"rounded-[14px]\";\n\n/** Row separator inside the white card. */\nexport const TABLE_ROW_BORDER_CLASS = \"border-table-row-border border-b\";\n\n/**\n * Keyboard focus ring for a body row, as inset shadows on its cells with\n * rounded ends. An outline on the row stays square and cuts across the\n * card's rounded corners.\n */\nexport const TABLE_ROW_FOCUS_RING_CLASS = [\n  \"focus-visible:[&>td]:shadow-[inset_0_2px_0_var(--ring),inset_0_-2px_0_var(--ring)]\",\n  \"focus-visible:[&>td:first-child]:shadow-[inset_2px_0_0_var(--ring),inset_0_2px_0_var(--ring),inset_0_-2px_0_var(--ring)]\",\n  \"focus-visible:[&>td:last-child]:shadow-[inset_-2px_0_0_var(--ring),inset_0_2px_0_var(--ring),inset_0_-2px_0_var(--ring)]\",\n  \"focus-visible:[&>td:only-child]:shadow-[inset_0_0_0_2px_var(--ring)]\",\n  // Rounded ends sit inside the card's 14px corner on the first and last row.\n  \"focus-visible:[&>td:first-child]:rounded-l-[12px] focus-visible:[&>td:last-child]:rounded-r-[12px]\",\n].join(\" \");\n\n/** Inner box for table cells. Gives children a real used width so truncate /\n * max-width:100% resolve against the column, not the td's max-w-0 hack. */\nexport const TABLE_CELL_INNER_CLASS =\n  \"w-full min-w-0 truncate [&>*]:min-w-0 [&>*]:max-w-full\";\n\nexport const CHECKBOX_COLUMN_WIDTH = \"3rem\";\n\n/**\n * Width the frame takes from the rows: shell border + 2px rim + body border,\n * on both sides. Add it to any outer width that has to fit a framed table.\n */\nexport const TABLE_FRAME_INSET = \"8px\";\n/** `TABLE_FRAME_INSET` as a number, for heights measured in px. */\nexport const TABLE_FRAME_INSET_PX = 8;\n\n/** Horizontal padding of header labels (`px-4` on both sides). */\nexport const HEADER_PAD_X_PX = 32;\n/** Sort arrow: 14px icon + 4px `gap-1`. */\nexport const SORT_ICON_PX = 18;\n/** Info icon plus its gap, so a hint never squeezes the header label. */\nexport const HINT_ICON_PX = 18;\n/** Default resize/layout floor, used as the tables' `minColumnWidth`. */\nexport const DEFAULT_MIN_COLUMN_WIDTH = 64;\n/** Extra `ch` so wide glyphs (M, W) are not clipped vs the `0`-width `ch` unit. */\nexport const HEADER_CH_BUFFER = 1;\n\nexport const DATA_TABLE_ROW_HEIGHT = 48;\n/** Column header band; slimmer than a row so the labels do not float in grey. */\nexport const DATA_TABLE_HEADER_HEIGHT = 36;\nexport const DATA_TABLE_HEIGHT = 440;\nexport const DATA_TABLE_OVERSCAN = 10;\nexport const DATA_TABLE_SKELETON_ROWS = 3;\n/** Distance from the bottom, in rows, at which an infinite table asks for more. */\nexport const INFINITE_TABLE_END_THRESHOLD_ROWS = 4;\n\nexport const TABLE_SKELETON_COLUMN_WIDTHS = [\"1fr\", \"1fr\", \"8rem\", \"6rem\"];\nexport const TABLE_SKELETON_ROW_COUNT = 5;\nexport const TABLE_SKELETON_ROW_HEIGHT = 52;\n\n/** Page sizes the table footer offers by default. */\nexport const TABLE_PAGE_SIZE_OPTIONS = [10, 20, 30, 40, 50] as const;\n\n/** Sideways overflow below this many px is rounding, not columns that don't fit. */\nexport const HORIZONTAL_OVERFLOW_TOLERANCE_PX = 2;\n\n/** Outer radius of the white card (`rounded-[14px]`). */\nexport const TABLE_BODY_RADIUS_PX = 14;\n/** Corner scrollbar: gap between the card's outer edge and the thumb. */\nexport const CORNER_SCROLLBAR_GAP_PX = 3;\n/** Corner scrollbar thumb width. */\nexport const CORNER_SCROLLBAR_WIDTH_PX = 4;\n/** Shell border (1px) plus rim (2px) between the shell edge and the body card. */\nexport const TABLE_SHELL_INSET_PX = 3;\n/** Shortest thumb, so a long table still has something to grab. */\nexport const CORNER_SCROLLBAR_MIN_THUMB_PX = 28;\n/** How long the thumb stays visible after the last scroll event. */\nexport const CORNER_SCROLLBAR_IDLE_MS = 900;\n\n/**\n * Corner-morph geometry as CSS variables, set on the table frame so both the\n * shell and the body read them. The tightened card radius is concentric with\n * the thumb's round end; the shell's adds its own inset on top.\n */\nexport const CORNER_SCROLLBAR_VARS = {\n  \"--corner-scrollbar-gap\": `${CORNER_SCROLLBAR_GAP_PX}px`,\n  \"--corner-scrollbar-width\": `${CORNER_SCROLLBAR_WIDTH_PX}px`,\n  \"--corner-card-radius\": `${CORNER_SCROLLBAR_GAP_PX + CORNER_SCROLLBAR_WIDTH_PX / 2}px`,\n  \"--corner-shell-radius\": `${CORNER_SCROLLBAR_GAP_PX + CORNER_SCROLLBAR_WIDTH_PX / 2 + TABLE_SHELL_INSET_PX}px`,\n} as CSSProperties;\n\nexport const DATA_TABLE_LABELS: DataTableLabels = {\n  table: \"Table\",\n  noData: \"No data\",\n  selectAllRows: \"Select all rows\",\n  selectRow: (row) => `Select row ${row}`,\n  resizeColumn: (column) => `Resize ${column} column`,\n  pagination: \"Pagination\",\n  goToPreviousPage: \"Go to previous page\",\n  goToNextPage: \"Go to next page\",\n  paginationRange: (start, end, total) => `${start}-${end} of ${total}`,\n  paginationRangeOpen: (start, end) => `${start}-${end}`,\n  pageOf: (page, total) => `Page ${page} of ${total}`,\n  pageNumber: (page) => `Page ${page}`,\n  showRows: (count) => `Show ${count}`,\n  rowsPerPage: \"Rows per page\",\n};\n\n/** Hidden element that resolves a CSS width to pixels inside a table. */\nexport const COLUMN_FLOOR_PROBE_STYLE =\n  \"position:absolute;visibility:hidden;pointer-events:none;height:0;overflow:hidden;\";\n",
      "type": "registry:lib",
      "target": "components/table/constants/data-table.ts"
    },
    {
      "path": "registry/notra/table/hooks/use-collapsible-columns.ts",
      "content": "import { useLayoutEffect, useMemo, useRef, useState } from \"react\";\n\nimport { measureColumnFloors, tableMinWidthCss } from \"../lib/data-table\";\nimport type { TableColumn } from \"../types/data-table\";\n\ninterface CollapsibleColumnsOptions {\n  minColumnWidth: number;\n  extraFixedWidths?: readonly string[];\n}\n\nfunction columnSignature<T>(column: TableColumn<T>): string {\n  const header = typeof column.header === \"string\" ? column.header : \"\";\n  return [\n    column.key,\n    column.width ?? \"\",\n    column.minWidth ?? \"\",\n    header,\n    column.sortable ? 1 : 0,\n    column.collapsePriority ?? \"\",\n  ].join(\":\");\n}\n\n/**\n * Drops low-priority columns while the container is narrower than the\n * remaining column floors. Floors are CSS lengths (rem, ch, calc), so the\n * candidate sets are measured together with hidden probes; resizing afterwards\n * only compares numbers and re-renders when the hidden set actually changes.\n */\nexport function useCollapsibleColumns<T>(\n  columns: TableColumn<T>[],\n  { minColumnWidth, extraFixedWidths = [] }: CollapsibleColumnsOptions\n) {\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const [hiddenCount, setHiddenCount] = useState(0);\n  const signature = columns.map(columnSignature).join(\"|\");\n  const chromeSignature = extraFixedWidths.join(\"|\");\n  const latest = useRef({ columns, extraFixedWidths });\n  useLayoutEffect(() => {\n    latest.current = { columns, extraFixedWidths };\n  });\n  const collapseKeys = columns\n    .filter((column) => column.collapsePriority !== undefined)\n    .sort(\n      (left, right) =>\n        (right.collapsePriority ?? 0) - (left.collapsePriority ?? 0)\n    )\n    .map((column) => column.key)\n    .join(\"|\");\n\n  useLayoutEffect(() => {\n    const container = containerRef.current;\n    const collapseOrder = collapseKeys ? collapseKeys.split(\"|\") : [];\n    if (!container || collapseOrder.length === 0) {\n      setHiddenCount(0);\n      return;\n    }\n    const { columns: current, extraFixedWidths: fixed } = latest.current;\n\n    const floorWidths = collapseOrder.map((_, count) => {\n      const hidden = new Set(collapseOrder.slice(0, count));\n      return tableMinWidthCss(\n        current.filter((column) => !hidden.has(column.key)),\n        minColumnWidth,\n        fixed\n      );\n    });\n    // A hidden container measures nothing; the observer measures it once it\n    // shows, since its size then changes.\n    let floors: readonly number[] | null = null;\n    const update = () => {\n      let containerWidth = container.clientWidth;\n      if (!floors) {\n        const measured = measureColumnFloors(container, floorWidths);\n        if (!measured) {\n          return;\n        }\n        floors = measured.floors;\n        containerWidth = measured.width;\n      }\n      const fits = floors.findIndex((floor) => floor <= containerWidth);\n      setHiddenCount(fits === -1 ? collapseOrder.length : fits);\n    };\n    update();\n    const observer = new ResizeObserver(() => update());\n    observer.observe(container);\n    return () => observer.disconnect();\n  }, [collapseKeys, signature, minColumnWidth, chromeSignature]);\n\n  const visibleColumns = useMemo(() => {\n    if (hiddenCount === 0) {\n      return columns;\n    }\n    const hidden = new Set(collapseKeys.split(\"|\").slice(0, hiddenCount));\n    return columns.filter((column) => !hidden.has(column.key));\n  }, [collapseKeys, columns, hiddenCount]);\n\n  return { containerRef, visibleColumns };\n}\n",
      "type": "registry:hook",
      "target": "components/table/hooks/use-collapsible-columns.ts"
    },
    {
      "path": "registry/notra/table/hooks/use-column-resize.ts",
      "content": "import {\n  type PointerEvent as ReactPointerEvent,\n  useCallback,\n  useRef,\n  useState,\n} from \"react\";\n\nimport type { HeaderCellRefs, TableColumn } from \"../types/data-table\";\n\nexport function useColumnResize<T>({\n  orderedColumns,\n  thRefs,\n  minColumnWidth,\n  onColumnResize,\n}: {\n  orderedColumns: TableColumn<T>[];\n  thRefs: HeaderCellRefs;\n  minColumnWidth: number;\n  onColumnResize?: (key: string, width: number) => void;\n}) {\n  const resizeRef = useRef<{\n    key: string;\n    startX: number;\n    startWidth: number;\n  } | null>(null);\n  const [widths, setWidths] = useState<Record<string, number>>({});\n\n  const startResize = useCallback(\n    (key: string, e: ReactPointerEvent) => {\n      e.preventDefault();\n      e.stopPropagation();\n      const snapshot = { ...widths };\n      for (const column of orderedColumns) {\n        if (snapshot[column.key] == null) {\n          const measured =\n            thRefs.current[column.key]?.getBoundingClientRect().width;\n          snapshot[column.key] = measured\n            ? Math.round(measured)\n            : minColumnWidth;\n        }\n      }\n      resizeRef.current = {\n        key,\n        startX: e.clientX,\n        startWidth: snapshot[key] ?? minColumnWidth,\n      };\n      setWidths(snapshot);\n      e.currentTarget.setPointerCapture(e.pointerId);\n    },\n    [minColumnWidth, orderedColumns, thRefs, widths]\n  );\n\n  const moveResize = useCallback(\n    (e: ReactPointerEvent) => {\n      const state = resizeRef.current;\n      if (!state) {\n        return;\n      }\n      const width = Math.max(\n        minColumnWidth,\n        state.startWidth + (e.clientX - state.startX)\n      );\n      setWidths((prev) => ({ ...prev, [state.key]: width }));\n    },\n    [minColumnWidth]\n  );\n\n  const endResize = useCallback(\n    (e: ReactPointerEvent) => {\n      const state = resizeRef.current;\n      resizeRef.current = null;\n      if (e.currentTarget.hasPointerCapture(e.pointerId)) {\n        e.currentTarget.releasePointerCapture(e.pointerId);\n      }\n      if (state) {\n        onColumnResize?.(state.key, widths[state.key] ?? state.startWidth);\n      }\n    },\n    [onColumnResize, widths]\n  );\n\n  return { widths, startResize, moveResize, endResize };\n}\n",
      "type": "registry:hook",
      "target": "components/table/hooks/use-column-resize.ts"
    },
    {
      "path": "registry/notra/table/hooks/use-column-sort.ts",
      "content": "import { useCallback, useMemo, useState } from \"react\";\n\nimport { readSortValue } from \"../lib/data-table\";\nimport type { SortState, TableColumn, TableRow } from \"../types/data-table\";\n\nexport function useColumnSort<T>({\n  rows,\n  columns,\n  sort: sortProp,\n  defaultSort = null,\n  onSortChange,\n  manualSort = false,\n}: {\n  rows: TableRow<T>[];\n  columns: TableColumn<T>[];\n  sort?: SortState | null;\n  defaultSort?: SortState | null;\n  onSortChange?: (sort: SortState | null) => void;\n  manualSort?: boolean;\n}) {\n  const [internalSort, setInternalSort] = useState<SortState | null>(\n    defaultSort\n  );\n  const sort = sortProp !== undefined ? sortProp : internalSort;\n\n  const commit = useCallback(\n    (next: SortState | null) => {\n      if (sortProp === undefined) {\n        setInternalSort(next);\n      }\n      onSortChange?.(next);\n    },\n    [sortProp, onSortChange]\n  );\n\n  const toggleSort = useCallback(\n    (key: string) => {\n      if (!sort || sort.key !== key) {\n        commit({ key, direction: \"asc\" });\n      } else if (sort.direction === \"asc\") {\n        commit({ key, direction: \"desc\" });\n      } else {\n        commit(null);\n      }\n    },\n    [sort, commit]\n  );\n\n  const sortedRows = useMemo(() => {\n    if (!sort || manualSort) {\n      return rows;\n    }\n    const column = columns.find((c) => c.key === sort.key);\n    if (!column) {\n      return rows;\n    }\n    const copy = [...rows];\n    copy.sort((a, b) => {\n      const av = readSortValue(a.row, column);\n      const bv = readSortValue(b.row, column);\n      let cmp: number;\n      if (typeof av === \"number\" && typeof bv === \"number\") {\n        cmp = av - bv;\n      } else {\n        cmp = String(av).localeCompare(String(bv));\n      }\n      return sort.direction === \"asc\" ? cmp : -cmp;\n    });\n    return copy;\n  }, [rows, sort, columns, manualSort]);\n\n  return { sort, sortedRows, toggleSort };\n}\n",
      "type": "registry:hook",
      "target": "components/table/hooks/use-column-sort.ts"
    },
    {
      "path": "registry/notra/table/hooks/use-row-selection.ts",
      "content": "import { useCallback, useMemo, useState } from \"react\";\n\nimport type { TableRow } from \"../types/data-table\";\n\nexport function useRowSelection<T>({\n  sortedRows,\n  selectedRowIds,\n  defaultSelectedRowIds,\n  onSelectionChange,\n}: {\n  sortedRows: TableRow<T>[];\n  selectedRowIds?: string[];\n  defaultSelectedRowIds?: string[];\n  onSelectionChange?: (ids: string[]) => void;\n}) {\n  const [internalSelected, setInternalSelected] = useState<Set<string>>(\n    () => new Set(defaultSelectedRowIds)\n  );\n  const selected = useMemo(\n    () =>\n      selectedRowIds !== undefined ? new Set(selectedRowIds) : internalSelected,\n    [selectedRowIds, internalSelected]\n  );\n\n  const commit = useCallback(\n    (next: Set<string>) => {\n      if (selectedRowIds === undefined) {\n        setInternalSelected(next);\n      }\n      onSelectionChange?.([...next]);\n    },\n    [selectedRowIds, onSelectionChange]\n  );\n\n  const allSelected =\n    sortedRows.length > 0 && sortedRows.every((r) => selected.has(r.id));\n  const someSelected = sortedRows.some((r) => selected.has(r.id));\n\n  const toggleAll = useCallback(() => {\n    const next = new Set(selected);\n    if (allSelected) {\n      for (const r of sortedRows) {\n        next.delete(r.id);\n      }\n    } else {\n      for (const r of sortedRows) {\n        next.add(r.id);\n      }\n    }\n    commit(next);\n  }, [allSelected, sortedRows, selected, commit]);\n\n  const toggleRow = useCallback(\n    (id: string) => {\n      const next = new Set(selected);\n      if (next.has(id)) {\n        next.delete(id);\n      } else {\n        next.add(id);\n      }\n      commit(next);\n    },\n    [selected, commit]\n  );\n\n  return { selected, allSelected, someSelected, toggleAll, toggleRow };\n}\n",
      "type": "registry:hook",
      "target": "components/table/hooks/use-row-selection.ts"
    },
    {
      "path": "registry/notra/table/hooks/use-table-paging.ts",
      "content": "import { useState } from \"react\";\n\nimport { pageRows } from \"../lib/data-table\";\nimport type { DataTablePagination, TableRow } from \"../types/data-table\";\n\n/**\n * Slices client-paged rows and owns the reader's page size when the caller\n * does not control it. Server and cursor pages arrive already sliced as\n * `data`, so they pass through untouched.\n */\nexport function useTablePaging<T>(\n  rows: TableRow<T>[],\n  pagination: DataTablePagination | undefined,\n  visibleRowCount: number | undefined\n) {\n  const [clientPageSize, setClientPageSize] = useState<number | null>(null);\n  const clientPaging =\n    pagination?.mode === undefined || pagination.mode === \"client\"\n      ? pagination\n      : undefined;\n  const ownsPageSize = Boolean(clientPaging && !clientPaging.onPageSizeChange);\n  const effective =\n    clientPaging && ownsPageSize && clientPageSize !== null\n      ? { ...clientPaging, pageSize: clientPageSize }\n      : clientPaging;\n\n  if (!effective) {\n    return {\n      pagedRows: pageRows(rows, 1, visibleRowCount),\n      pagination,\n      onClientPageSizeChange: undefined,\n    };\n  }\n  const pageCount = Math.max(1, Math.ceil(rows.length / effective.pageSize));\n  const page = Math.min(Math.max(1, effective.page), pageCount);\n  return {\n    pagedRows: pageRows(rows, page, effective.pageSize),\n    pagination: effective,\n    onClientPageSizeChange: ownsPageSize ? setClientPageSize : undefined,\n  };\n}\n",
      "type": "registry:hook",
      "target": "components/table/hooks/use-table-paging.ts"
    },
    {
      "path": "registry/notra/table/hooks/use-table-viewport.ts",
      "content": "import { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport {\n  HORIZONTAL_OVERFLOW_TOLERANCE_PX,\n  INFINITE_TABLE_END_THRESHOLD_ROWS,\n} from \"../constants/data-table\";\nimport {\n  getTableScrollbarHeight,\n  getTableViewportLayout,\n} from \"../lib/data-table\";\nimport type { UseTableViewportOptions } from \"../types/data-table\";\n\nexport function useTableViewport<T>({\n  rows,\n  rowHeight,\n  headerHeight,\n  rowSizing,\n  height,\n  minHeight,\n  autoHeight,\n  overscan,\n  loading,\n  onEndReached,\n}: UseTableViewportOptions<T>) {\n  \"use no memo\";\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const headerScrollRef = useRef<HTMLDivElement>(null);\n  const endReachedRef = useRef(false);\n  const [horizontalScrollbarHeight, setHorizontalScrollbarHeight] = useState(0);\n  const [overflowsX, setOverflowsX] = useState(false);\n\n  const layout = getTableViewportLayout({\n    rowCount: rows.length,\n    rowHeight,\n    headerHeight,\n    rowSizing,\n    height,\n    minHeight,\n    autoHeight,\n    horizontalScrollbarHeight,\n  });\n  const virtualizer = useVirtualizer({\n    // A table that shows all its rows has nothing to virtualize, and the\n    // virtualizer would measure its scroll element on every mount.\n    enabled: layout.scrolls,\n    count: rows.length,\n    getScrollElement: () => scrollRef.current,\n    estimateSize: () => rowHeight,\n    initialRect: { height, width: 0 },\n    overscan,\n  });\n  const virtualItems = virtualizer.getVirtualItems();\n  const totalSize = virtualizer.getTotalSize();\n  const paddingTop = virtualItems[0]?.start ?? 0;\n  const lastVirtualItem = virtualItems.at(-1);\n  const paddingBottom = lastVirtualItem ? totalSize - lastVirtualItem.end : 0;\n  const renderedRows = layout.scrolls\n    ? virtualItems.flatMap((item) => {\n        const entry = rows[item.index];\n        return entry ? [{ entry, index: item.index }] : [];\n      })\n    : rows.map((entry, index) => ({ entry, index }));\n\n  useEffect(() => {\n    if (!loading) {\n      endReachedRef.current = false;\n    }\n  }, [loading]);\n\n  useEffect(() => {\n    endReachedRef.current = false;\n  }, [rows.length]);\n\n  const [atEnd, setAtEnd] = useState(true);\n  const [atStart, setAtStart] = useState(true);\n\n  const requestMoreIfNearEnd = useCallback(\n    (element: HTMLDivElement) => {\n      if (!onEndReached || loading || endReachedRef.current) {\n        return;\n      }\n      if (\n        element.scrollHeight - element.scrollTop - element.clientHeight <\n        rowHeight * INFINITE_TABLE_END_THRESHOLD_ROWS\n      ) {\n        endReachedRef.current = true;\n        onEndReached();\n      }\n    },\n    [onEndReached, loading, rowHeight]\n  );\n\n  const handleScroll = useCallback(() => {\n    const element = scrollRef.current;\n    if (!element) {\n      return;\n    }\n    if (headerScrollRef.current) {\n      headerScrollRef.current.scrollLeft = element.scrollLeft;\n    }\n    setAtEnd(\n      element.scrollTop + element.clientHeight + 1 >= element.scrollHeight\n    );\n    setAtStart(element.scrollTop <= 1);\n    requestMoreIfNearEnd(element);\n  }, [requestMoreIfNearEnd]);\n\n  useEffect(() => {\n    const element = scrollRef.current;\n    if (!element) {\n      return;\n    }\n    const measure = () => {\n      setAtEnd(\n        element.scrollTop + element.clientHeight + 1 >= element.scrollHeight\n      );\n      setAtStart(element.scrollTop <= 1);\n      requestMoreIfNearEnd(element);\n    };\n    // A new observer reports the current size once, after the browser's own\n    // layout, so mounting (or showing a hidden Activity) forces no layout.\n    const observer = new ResizeObserver(measure);\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, [rows.length, requestMoreIfNearEnd]);\n\n  useEffect(() => {\n    const element = scrollRef.current;\n    if (!element) {\n      return;\n    }\n    const measure = () => {\n      const styles = getComputedStyle(element);\n      setHorizontalScrollbarHeight(\n        getTableScrollbarHeight(\n          element.offsetHeight,\n          element.clientHeight,\n          styles.borderTopWidth,\n          styles.borderBottomWidth\n        )\n      );\n    };\n    const observer = new ResizeObserver(measure);\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, []);\n\n  useEffect(() => {\n    const element = scrollRef.current;\n    const table = element?.querySelector(\"table\");\n    if (!(element && table)) {\n      return;\n    }\n    const measure = () => {\n      setOverflowsX(\n        element.scrollWidth - element.clientWidth >\n          HORIZONTAL_OVERFLOW_TOLERANCE_PX\n      );\n    };\n    const observer = new ResizeObserver(measure);\n    observer.observe(element);\n    observer.observe(table);\n    return () => observer.disconnect();\n  }, []);\n\n  return {\n    ...layout,\n    overflowClass: overflowsX\n      ? layout.overflowClass\n      : layout.overflowClass\n          .replace(\"overflow-x-auto\", \"overflow-x-hidden\")\n          .replace(\"overflow-auto\", \"overflow-x-hidden overflow-y-auto\"),\n    scrollRef,\n    headerScrollRef,\n    handleScroll,\n    renderedRows,\n    paddingTop,\n    paddingBottom,\n    atEnd,\n    atStart,\n    overflowsX,\n  };\n}\n",
      "type": "registry:hook",
      "target": "components/table/hooks/use-table-viewport.ts"
    },
    {
      "path": "registry/notra/table/lib/data-table.ts",
      "content": "import type { ReactNode } from \"react\";\n\nimport {\n  COLUMN_FLOOR_PROBE_STYLE,\n  DATA_TABLE_HEADER_HEIGHT,\n  HEADER_CH_BUFFER,\n  HEADER_PAD_X_PX,\n  HINT_ICON_PX,\n  TABLE_PAGE_SIZE_OPTIONS,\n  SORT_ICON_PX,\n} from \"../constants/data-table\";\nimport type {\n  TableColumn,\n  TableLoadingOverlay,\n  TableRow,\n  TableViewportLayout,\n  TableViewportLayoutOptions,\n} from \"../types/data-table\";\n\nexport function alignFlex(align: TableColumn<unknown>[\"align\"]) {\n  if (align === \"right\") {\n    return \"justify-end\";\n  }\n  if (align === \"center\") {\n    return \"justify-center\";\n  }\n  return \"justify-start\";\n}\n\nexport function alignText(align: TableColumn<unknown>[\"align\"]) {\n  if (align === \"right\") {\n    return \"text-right\";\n  }\n  if (align === \"center\") {\n    return \"text-center\";\n  }\n  return \"text-left\";\n}\n\nexport function readCell<T>(row: T, column: TableColumn<T>): ReactNode {\n  if (column.cell) {\n    return column.cell(row);\n  }\n  return (row as Record<string, ReactNode>)[column.key];\n}\n\nexport function readSortValue<T>(\n  row: T,\n  column: TableColumn<T>\n): string | number {\n  if (column.sortValue) {\n    return column.sortValue(row);\n  }\n  return (row as Record<string, string | number>)[column.key] ?? \"\";\n}\n\n/** Slice `rows` to the current page; unpaged when `pageSize` is unset. */\nexport function pageRows<T>(\n  rows: TableRow<T>[],\n  page: number,\n  pageSize?: number\n): TableRow<T>[] {\n  if (pageSize == null) {\n    return rows;\n  }\n  const pageStart = Math.max(0, page - 1) * pageSize;\n  return rows.slice(pageStart, pageStart + pageSize);\n}\n\n/** After column sort, move matching rows to the front without pinning them sticky. */\nexport function pinRowsFirst<T>(\n  rows: readonly TableRow<T>[],\n  isRowPinned?: (row: T) => boolean\n): TableRow<T>[] {\n  if (!isRowPinned) {\n    return [...rows];\n  }\n  const pinned: TableRow<T>[] = [];\n  const rest: TableRow<T>[] = [];\n  for (const entry of rows) {\n    if (isRowPinned(entry.row)) {\n      pinned.push(entry);\n    } else {\n      rest.push(entry);\n    }\n  }\n  return pinned.length === 0 ? [...rows] : [...pinned, ...rest];\n}\n\nconst FR_WIDTH_REGEX = /^([\\d.]+)fr$/;\nconst PERCENT_WIDTH_REGEX = /^([\\d.]+)%$/;\nconst FULL_PERCENT = 100;\n\nconst PERCENT_DECIMALS = 4;\n\nexport function isFrWidth(width: string | undefined): boolean {\n  return width != null && FR_WIDTH_REGEX.test(width);\n}\n\nexport function headerMinWidth(\n  column: Pick<\n    TableColumn<unknown>,\n    \"header\" | \"hint\" | \"sortable\" | \"minWidth\"\n  >,\n  minColumnWidth: number\n): string {\n  if (column.minWidth) {\n    return column.minWidth;\n  }\n  const chromePx =\n    HEADER_PAD_X_PX +\n    (column.sortable ? SORT_ICON_PX : 0) +\n    (column.hint ? HINT_ICON_PX : 0);\n  if (typeof column.header === \"string\" && column.header.length > 0) {\n    return `max(${minColumnWidth}px, calc(${column.header.length + HEADER_CH_BUFFER}ch + ${chromePx}px))`;\n  }\n  return `${minColumnWidth}px`;\n}\n\n/**\n * The width a column actually occupies at its narrowest: fixed widths never\n * shrink below their declared size (see `colWidthStyle`), flexible ones stop\n * at their header floor.\n */\nexport function columnFloorCss<T>(\n  column: Pick<\n    TableColumn<T>,\n    \"header\" | \"hint\" | \"sortable\" | \"minWidth\" | \"width\"\n  >,\n  minColumnWidth: number\n): string {\n  const floor = headerMinWidth(column, minColumnWidth);\n  if (\n    !column.width ||\n    isFrWidth(column.width) ||\n    percentWidth(column.width) != null\n  ) {\n    return floor;\n  }\n  return `max(${column.width}, ${floor})`;\n}\n\n/** Share of the table a `\"20%\"` width takes, or null for any other width. */\nfunction percentWidth(width: string | undefined): number | null {\n  const match = width ? PERCENT_WIDTH_REGEX.exec(width) : null;\n  return match ? Number.parseFloat(match[1] ?? \"0\") : null;\n}\n\n/**\n * Sum of column floors so `table-layout: fixed` cannot crush titles. Fixed\n * widths count in full: WebKit fits a fixed table to its `min-width`, so a\n * smaller sum squeezes the flexible columns to nothing on narrow screens.\n */\nexport function tableMinWidthCss<T>(\n  columns: readonly Pick<\n    TableColumn<T>,\n    \"header\" | \"hint\" | \"sortable\" | \"minWidth\" | \"width\"\n  >[],\n  minColumnWidth: number,\n  extraFixedWidths: readonly string[] = []\n): string {\n  const floors = columns.map((column) =>\n    columnFloorCss(column, minColumnWidth)\n  );\n  const parts = [...extraFixedWidths, ...floors];\n  if (parts.length === 0) {\n    return \"0px\";\n  }\n  const sum =\n    parts.length === 1 ? (parts[0] ?? \"0px\") : `calc(${parts.join(\" + \")})`;\n\n  const percentTotal = columns.reduce(\n    (total, column) => total + (percentWidth(column.width) ?? 0),\n    0\n  );\n  if (percentTotal === 0 || percentTotal >= FULL_PERCENT) {\n    return sum;\n  }\n  const rest = [\n    ...extraFixedWidths,\n    ...floors.filter((_, index) => percentWidth(columns[index]?.width) == null),\n  ];\n  const candidates = [sum];\n  if (rest.length > 0) {\n    candidates.push(\n      `calc((${rest.join(\" + \")}) * ${FULL_PERCENT / (FULL_PERCENT - percentTotal)})`\n    );\n  }\n  for (const [index, column] of columns.entries()) {\n    const share = percentWidth(column.width);\n    if (share) {\n      candidates.push(`calc(${floors[index]} * ${FULL_PERCENT / share})`);\n    }\n  }\n  return `max(${candidates.join(\", \")})`;\n}\n\nexport function colWidthStyle(\n  width: string | undefined,\n  flexible: boolean,\n  minWidth: string\n): { width?: string; minWidth: string } {\n  if (!width) {\n    return { minWidth };\n  }\n  if (flexible) {\n    return { width, minWidth };\n  }\n  return { width, minWidth: `max(${width}, ${minWidth})` };\n}\n\nexport function resolveColumnWidths<T>(\n  columns: readonly TableColumn<T>[],\n  extraFixedWidths: readonly string[] = []\n): (string | undefined)[] {\n  const hasFr = columns.some((column) => isFrWidth(column.width));\n  const frValues = columns.map((column) => {\n    if (!column.width) {\n      return hasFr ? 1 : null;\n    }\n    const match = FR_WIDTH_REGEX.exec(column.width);\n    return match ? Number.parseFloat(match[1] ?? \"0\") : null;\n  });\n  const totalFr = frValues.reduce<number>(\n    (sum, value) => sum + (value ?? 0),\n    0\n  );\n  if (totalFr === 0) {\n    return columns.map((column) => column.width);\n  }\n\n  const fixedWidths = [\n    ...extraFixedWidths,\n    ...columns.flatMap((column, index) =>\n      frValues[index] == null && column.width ? [column.width] : []\n    ),\n  ];\n  const remainder =\n    fixedWidths.length > 0 ? `100% - ${fixedWidths.join(\" - \")}` : null;\n\n  return columns.map((column, index) => {\n    const fr = frValues[index];\n    if (fr === null || fr === undefined) {\n      return column.width;\n    }\n    if (!remainder) {\n      return `${((fr / totalFr) * 100).toFixed(PERCENT_DECIMALS)}%`;\n    }\n    return `calc((${remainder}) * ${fr} / ${totalFr})`;\n  });\n}\n\n/** Dim existing rows, append skeletons for load-more, or fill an empty table. */\nexport function tableLoadingOverlay(\n  loading: boolean,\n  rowCount: number,\n  loadingMoreProp: boolean | undefined,\n  hasEndReached: boolean\n): TableLoadingOverlay {\n  const hasRows = rowCount > 0;\n  const loadingMore = loading && hasRows && (loadingMoreProp ?? hasEndReached);\n  const dimRows = loading && hasRows && !loadingMore;\n  if (dimRows) {\n    return { loadingMore, dimRows, loadingState: \"dimmed\" };\n  }\n  if (loadingMore) {\n    return { loadingMore, dimRows, loadingState: \"more\" };\n  }\n  if (loading) {\n    return { loadingMore, dimRows, loadingState: \"skeleton\" };\n  }\n  return { loadingMore, dimRows, loadingState: undefined };\n}\n\nexport function getTableScrollbarHeight(\n  offsetHeight: number,\n  clientHeight: number,\n  borderTopWidth: string,\n  borderBottomWidth: string\n): number {\n  const borders =\n    (Number.parseFloat(borderTopWidth) || 0) +\n    (Number.parseFloat(borderBottomWidth) || 0);\n  return Math.max(0, offsetHeight - clientHeight - borders);\n}\n\nexport function getTableViewportLayout({\n  rowCount,\n  rowHeight,\n  headerHeight = DATA_TABLE_HEADER_HEIGHT,\n  rowSizing,\n  height,\n  minHeight,\n  autoHeight = false,\n  horizontalScrollbarHeight,\n}: TableViewportLayoutOptions): TableViewportLayout {\n  const resolvedHeight = Math.max(height, minHeight ?? 0);\n  const bodyHeight =\n    Math.floor(Math.max(rowHeight, resolvedHeight - headerHeight) / rowHeight) *\n    rowHeight;\n  const minBodyHeight =\n    minHeight == null\n      ? 0\n      : Math.floor(Math.max(rowHeight, minHeight - headerHeight) / rowHeight) *\n        rowHeight;\n  const contentHeight = Math.max(\n    rowHeight,\n    Math.min(bodyHeight, rowCount * rowHeight)\n  );\n  const contentSized = rowSizing === \"content\";\n  const scrolls = !contentSized && rowCount * rowHeight > bodyHeight;\n  const viewportHeight = scrolls\n    ? bodyHeight\n    : Math.max(rowCount === 0 ? bodyHeight : contentHeight, minBodyHeight);\n  const scrollbarGutter = contentSized || scrolls ? \"stable\" : undefined;\n\n  if (contentSized && autoHeight && rowCount > 0) {\n    return {\n      bodyHeight,\n      scrolls: false,\n      overflowClass: \"overflow-x-auto overflow-y-hidden\",\n      headerStyle: undefined,\n      bodyStyle: { minHeight: minBodyHeight },\n    };\n  }\n\n  return {\n    bodyHeight,\n    scrolls,\n    overflowClass:\n      scrolls || contentSized\n        ? \"overflow-auto\"\n        : \"overflow-x-auto overflow-y-hidden\",\n    headerStyle: scrollbarGutter ? { scrollbarGutter } : undefined,\n    bodyStyle:\n      contentSized && rowCount > 0\n        ? {\n            scrollbarGutter,\n            maxHeight: bodyHeight + horizontalScrollbarHeight,\n            minHeight: minBodyHeight,\n          }\n        : {\n            scrollbarGutter,\n            height: viewportHeight + horizontalScrollbarHeight,\n          },\n  };\n}\n\n/**\n * A page size from an untrusted source (URL, storage) snapped to one of the\n * offered sizes, so `?pageSize=0` or `?pageSize=9999` can't break paging or a\n * request with a bounded limit.\n */\nexport function normalizePageSize(\n  value: number,\n  fallback: number,\n  options: readonly number[] = TABLE_PAGE_SIZE_OPTIONS\n): number {\n  return options.includes(value) ? value : fallback;\n}\n\n/** Class and inline style shared by the header and body `<table>`s. */\nexport function tableLayout<T>(\n  columns: readonly TableColumn<T>[],\n  widths: Record<string, number>,\n  minColumnWidth: number,\n  extraFixedWidths: readonly string[]\n) {\n  const sized =\n    columns.length > 0 && columns.every((column) => widths[column.key] != null);\n  const floorColumns = columns.map((column) => {\n    const override = widths[column.key];\n    return override == null ? column : { ...column, width: `${override}px` };\n  });\n  return {\n    className: sized ? \"w-max min-w-full\" : \"w-full\",\n    style: {\n      tableLayout: \"fixed\" as const,\n      minWidth: tableMinWidthCss(\n        floorColumns,\n        minColumnWidth,\n        extraFixedWidths\n      ),\n    },\n  };\n}\n\n/**\n * Pixel floors per set of CSS widths, keyed by the fonts they resolve\n * against. Tables remount with the same columns on every filter or range\n * change; after the first measurement they only read their own width.\n */\nconst columnFloorCache = new Map<string, readonly number[]>();\nlet columnFloorCacheWatchesFonts = false;\n\n/**\n * Resolves each CSS width to pixels with hidden probes inside `container`, all\n * in one layout, and reads the container's width in that same layout. Returns\n * null while the container is not rendered (e.g. a tab hidden with\n * `display: none`): its probes would measure 0 and must not be kept.\n */\nexport function measureColumnFloors(\n  container: HTMLElement,\n  widths: readonly string[]\n): { floors: readonly number[]; width: number } | null {\n  if (container.getClientRects().length === 0) {\n    return null;\n  }\n  const key = [\n    getComputedStyle(document.documentElement).fontSize,\n    getComputedStyle(container).font,\n    ...widths,\n  ].join(\"|\");\n  const cached = columnFloorCache.get(key);\n  if (cached) {\n    return { floors: cached, width: container.clientWidth };\n  }\n  if (!columnFloorCacheWatchesFonts) {\n    columnFloorCacheWatchesFonts = true;\n    document.fonts.addEventListener(\"loadingdone\", () =>\n      columnFloorCache.clear()\n    );\n  }\n  const probes = widths.map((width) => {\n    const probe = document.createElement(\"div\");\n    probe.style.cssText = COLUMN_FLOOR_PROBE_STYLE;\n    probe.style.width = width;\n    return probe;\n  });\n  container.append(...probes);\n  const width = container.clientWidth;\n  const floors = probes.map((probe) => probe.offsetWidth);\n  for (const probe of probes) {\n    probe.remove();\n  }\n  columnFloorCache.set(key, floors);\n  return { floors, width };\n}\n",
      "type": "registry:lib",
      "target": "components/table/lib/data-table.ts"
    },
    {
      "path": "registry/notra/table/types/data-table.ts",
      "content": "import type {\n  CSSProperties,\n  PointerEvent as ReactPointerEvent,\n  ReactNode,\n  RefObject,\n  UIEventHandler,\n} from \"react\";\n\ntype SortDirection = \"asc\" | \"desc\";\n\nexport interface TablePaginationRange {\n  start: number;\n  end: number;\n  /** Unknown with cursor paging. */\n  total?: number;\n}\n\n/** Every string the table renders. Pass a partial set to translate it. */\n/** Every string the table renders. Pass a partial set to translate it. */\nexport interface DataTableLabels {\n  /** BCP 47 locale for numbers in the footer. */\n  locale?: string;\n  table: string;\n  noData: string;\n  selectAllRows: string;\n  selectRow: (row: string) => string;\n  resizeColumn: (column: string) => string;\n  pagination: string;\n  goToPreviousPage: string;\n  goToNextPage: string;\n  paginationRange: (start: string, end: string, total: string) => string;\n  paginationRangeOpen: (start: string, end: string) => string;\n  pageOf: (page: string, total: string) => string;\n  pageNumber: (page: string) => string;\n  showRows: (count: string) => string;\n  rowsPerPage: string;\n}\n\nexport interface SortState {\n  key: string;\n  direction: SortDirection;\n}\n\nexport interface TableColumn<T> {\n  /** Stable key; also the default object property read for the cell + sort value. */\n  key: string;\n  /** Header content. */\n  header: ReactNode;\n  /**\n   * Explains what the column counts, on an info icon beside the header. For\n   * columns whose name alone leaves the reader guessing how they relate to\n   * their neighbours.\n   */\n  hint?: ReactNode;\n  /** Allow clicking the header to sort by this column. */\n  sortable?: boolean;\n  /** Cell text alignment. */\n  align?: \"left\" | \"center\" | \"right\";\n  /** Vertical cell alignment in content-sized rows. Defaults to \"top\". */\n  verticalAlign?: \"top\" | \"middle\";\n  /** Column width as a CSS length, e.g. \"160px\", \"20%\" or \"2fr\". Omit to share remaining space equally. */\n  width?: string;\n  /** Floor for this column. Defaults to the header label plus sort/padding chrome so titles never ellipsize. */\n  minWidth?: string;\n  /**\n   * Lets the column drop out when the table is narrower than its column\n   * floors, instead of scrolling sideways. Higher numbers are hidden first;\n   * columns without a priority always stay.\n   */\n  collapsePriority?: number;\n  /** Custom cell renderer. Falls back to `row[key]`. */\n  cell?: (row: T) => ReactNode;\n  /** Value used for sorting. Falls back to `row[key]`. */\n  sortValue?: (row: T) => string | number;\n}\n\ninterface DataTablePaginationBase {\n  /** 1-based current page. */\n  page: number;\n  pageSize: number;\n  onPageChange: (page: number) => void;\n  itemLabel?: string;\n  formatRange?: (range: TablePaginationRange) => string;\n  /**\n   * Lets the reader pick the page size in the footer. Client paging works\n   * without it (the table keeps the choice); server and cursor paging need it\n   * to refetch, and fall back to the range text when it is missing.\n   */\n  onPageSizeChange?: (pageSize: number) => void;\n  /** Page sizes offered in the footer select. */\n  pageSizeOptions?: readonly number[];\n  /** Set false to hide the page size select, e.g. on fixed-height public tables. */\n  pageSizeSelector?: boolean;\n}\n\n/** Every row is loaded; the table slices `data` after sorting. */\nexport interface DataTableClientPagination extends DataTablePaginationBase {\n  mode?: \"client\";\n}\n\n/** `data` is already the current page; the server knows the total. */\nexport interface DataTableServerPagination extends DataTablePaginationBase {\n  mode: \"server\";\n  totalItems: number;\n}\n\n/** `data` is the current page and only \"is there more\" is known. */\nexport interface DataTableCursorPagination extends DataTablePaginationBase {\n  mode: \"cursor\";\n  hasNextPage: boolean;\n}\n\nexport type DataTablePagination =\n  | DataTableClientPagination\n  | DataTableServerPagination\n  | DataTableCursorPagination;\n\nexport interface DataTableProps<T> {\n  data: readonly T[];\n  columns: TableColumn<T>[];\n  /** Stable id per row, required for correct selection across sorts. Defaults to row index. */\n  getRowId?: (row: T, index: number) => string;\n  /** Render a leading checkbox column with select-all in the header. */\n  selectable?: boolean;\n  selectedRowIds?: string[];\n  defaultSelectedRowIds?: string[];\n  onSelectionChange?: (ids: string[]) => void;\n  sort?: SortState | null;\n  defaultSort?: SortState | null;\n  onSortChange?: (sort: SortState | null) => void;\n  /** Rows already arrive sorted (for example by the server); header clicks only report the sort. */\n  manualSort?: boolean;\n  /** Allow dragging the right edge of a header to resize that column. */\n  resizable?: boolean;\n  /** Minimum column width in px when resizing. */\n  minColumnWidth?: number;\n  onColumnResize?: (key: string, width: number) => void;\n  /** Fixed row height in px — required for virtualization. */\n  rowHeight?: number;\n  /** Header row height in px; defaults to `DATA_TABLE_HEADER_HEIGHT`. */\n  headerHeight?: number;\n  /** Content-sized rows wrap and render without virtualization. Use for bounded detail lists. */\n  rowSizing?: \"fixed\" | \"content\";\n  /** Scroll viewport height in px. */\n  height?: number;\n  /** Floor for the table body when there are fewer rows than `height` allows. */\n  minHeight?: number;\n  /**\n   * Content-sized rows only: grow with the rows instead of capping at\n   * `height`, so the body never scrolls on its own. `height` still sizes the\n   * empty and loading states.\n   */\n  autoHeight?: boolean;\n  /** Rows rendered above/below the viewport. */\n  overscan?: number;\n  /**\n   * Currently fetching. Empty tables fill with skeleton rows; tables that\n   * already have rows dim in place.\n   */\n  loading?: boolean;\n  /** How many skeleton rows to show while loading more (default 3). */\n  skeletonRows?: number;\n  /** Called when a row is clicked or activated with Enter/Space. */\n  onRowClick?: (row: T) => void;\n  /** Set false when a native control inside the row provides its keyboard action. */\n  rowKeyboardActivation?: boolean;\n  /** Only matching rows receive click handlers, keyboard activation, and pointer styling. */\n  isRowClickable?: (row: T) => boolean;\n  /** Extra classes for a row, e.g. to animate rows that just arrived. */\n  getRowClassName?: (row: T) => string | undefined;\n  /** Menu content shown when a row is opened with the context-menu gesture. */\n  renderRowContextMenu?: (row: T) => ReactNode;\n  /** Full-width row rendered under a row; return null to keep it collapsed. */\n  renderRowDetail?: (row: T) => ReactNode;\n  /** Called when a pointer enters a row — prefetch, hover menus, etc. */\n  onRowPointerEnter?: (row: T) => void;\n  /** Keep matching rows first after sort. They scroll with the table (not sticky). */\n  isRowPinned?: (row: T) => boolean;\n  /** Content rendered above the column headers inside the table surface. */\n  toolbar?: ReactNode;\n  /** Content rendered below the body on the shell, mirroring the header. */\n  footer?: ReactNode;\n  /** Page the rows and render the pager in the footer. Omit to render every row. */\n  pagination?: DataTablePagination;\n  emptyState?: ReactNode;\n  /** Join the shell of the table above: no top border or radius, rim kept. */\n  flushTop?: boolean;\n  /** Join the shell of the table below: no bottom border or radius, rim kept. */\n  flushBottom?: boolean;\n  /** Fade the top and bottom edges while more rows are scrolled out of view. Default on. */\n  scrollFade?: boolean;\n  /** Overrides for the table's strings, e.g. translations. */\n  labels?: Partial<DataTableLabels>;\n  className?: string;\n}\n\nexport interface InfiniteDataTableProps<T> extends Omit<\n  DataTableProps<T>,\n  \"rowSizing\" | \"pagination\"\n> {\n  /** Render only the first N rows after sorting; grow it to reveal more. */\n  visibleRowCount?: number;\n  /** Fires when the viewport scrolls near the bottom. Clear it when there is no next page. */\n  onEndReached: (() => void) | undefined;\n  /** Next page is being fetched; appends `skeletonRows` at the bottom. */\n  loadingMore?: boolean;\n}\n\n/** Engine props shared by both public tables. */\nexport interface DataTableRootProps<T> extends DataTableProps<T> {\n  visibleRowCount?: number;\n  onEndReached?: () => void;\n  loadingMore?: boolean;\n}\n\n/** A data row paired with its stable id. */\nexport interface TableRow<T> {\n  row: T;\n  id: string;\n}\n\n/** Ref map from column key to its header cell, shared across the resize hook. */\nexport interface HeaderCellRefs {\n  current: Record<string, HTMLTableCellElement | null>;\n}\n\n/** Layout of one skeleton column: a CSS width, or the sizing fields of the real column. */\nexport type DataTableSkeletonColumn =\n  | string\n  | Pick<\n      TableColumn<unknown>,\n      \"width\" | \"minWidth\" | \"collapsePriority\" | \"align\"\n    >;\n\nexport interface DataTableSkeletonProps {\n  columnWidths?: readonly DataTableSkeletonColumn[];\n  rows?: number;\n  rowHeight?: number;\n  /** Fixed table height; defaults to the header plus `rows` rows. */\n  height?: number;\n  /** Reserve the checkbox column of a selectable table. */\n  selectable?: boolean;\n  toolbar?: ReactNode;\n  className?: string;\n}\n\nexport interface TableHeaderSurfaceProps extends Pick<\n  DataTableProps<unknown>,\n  \"toolbar\" | \"flushTop\"\n> {\n  children: ReactNode;\n}\n\nexport interface TableFrameProps extends Pick<\n  DataTableProps<unknown>,\n  \"flushTop\" | \"flushBottom\"\n> {\n  children: ReactNode;\n}\n\nexport type TableFooterSurfaceProps = Pick<DataTableProps<unknown>, \"footer\">;\n\nexport interface TableScrollFadeProps extends Pick<\n  DataTableProps<unknown>,\n  \"scrollFade\"\n> {\n  edge: \"top\" | \"bottom\";\n  /** No rows hidden past this edge, so the fade is not shown. */\n  hidden: boolean;\n}\n\nexport type TableLoadingState = \"dimmed\" | \"more\" | \"skeleton\";\n\nexport interface TableLoadingOverlay {\n  loadingMore: boolean;\n  dimRows: boolean;\n  loadingState: TableLoadingState | undefined;\n}\n\nexport interface TableBodySurfaceProps {\n  isEmpty: boolean;\n  overflowClass: string;\n  dimRows: boolean;\n  loadingState: TableLoadingState | undefined;\n  onScroll: UIEventHandler<HTMLDivElement>;\n  scrollRef: RefObject<HTMLDivElement | null>;\n  style: CSSProperties;\n  /**\n   * Draw `TableCornerScrollbar` instead of the native scrollbar. Off when the\n   * rows scroll sideways, since it only covers the vertical axis.\n   */\n  cornerScrollbar: boolean;\n  children: ReactNode;\n}\n\nexport interface TableCornerScrollbarProps {\n  scrollId: string;\n  scrollRef: RefObject<HTMLDivElement | null>;\n  /** Element that receives the hover listeners and the corner data attributes. */\n  wrapperRef: RefObject<HTMLDivElement | null>;\n}\n\nexport interface TableColumnGroupProps<T> {\n  columns: TableColumn<T>[];\n  widths: Record<string, number>;\n  selectable: boolean;\n  minColumnWidth: number;\n}\n\nexport interface DataTableHeaderProps<T> {\n  columns: TableColumn<T>[];\n  rowHeight: number;\n  thRefs: HeaderCellRefs;\n  selectable: boolean;\n  allSelected: boolean;\n  someSelected: boolean;\n  onToggleAll: () => void;\n  sort: SortState | null;\n  onToggleSort: (key: string) => void;\n  resizable: boolean;\n  minColumnWidth: number;\n  onResizeStart: (key: string, e: ReactPointerEvent) => void;\n  onResizeMove: (e: ReactPointerEvent) => void;\n  onResizeEnd: (e: ReactPointerEvent) => void;\n}\n\nexport interface TableViewportLayoutOptions {\n  rowCount: number;\n  rowHeight: number;\n  headerHeight?: number;\n  rowSizing: NonNullable<DataTableProps<unknown>[\"rowSizing\"]>;\n  height: number;\n  minHeight?: number;\n  autoHeight?: boolean;\n  horizontalScrollbarHeight: number;\n}\n\nexport interface TableViewportLayout {\n  bodyHeight: number;\n  scrolls: boolean;\n  overflowClass: string;\n  bodyStyle: CSSProperties;\n  headerStyle: CSSProperties | undefined;\n}\n\nexport interface UseTableViewportOptions<T> extends Omit<\n  TableViewportLayoutOptions,\n  \"rowCount\" | \"horizontalScrollbarHeight\"\n> {\n  rows: TableRow<T>[];\n  overscan: number;\n  loading: boolean;\n  onEndReached?: () => void;\n}\n\nexport interface DataTableBodyProps<T> extends Pick<\n  DataTableProps<T>,\n  | \"onRowClick\"\n  | \"rowKeyboardActivation\"\n  | \"isRowClickable\"\n  | \"getRowClassName\"\n  | \"onRowPointerEnter\"\n  | \"renderRowContextMenu\"\n  | \"renderRowDetail\"\n  | \"emptyState\"\n  | \"rowSizing\"\n> {\n  columns: TableColumn<T>[];\n  renderedRows: { entry: TableRow<T>; index: number }[];\n  rowCount: number;\n  rowHeight: number;\n  bodyHeight: number;\n  loading: boolean;\n  loadingMore: boolean;\n  skeletonRows: number;\n  selectable: boolean;\n  selected: Set<string>;\n  scrolls: boolean;\n  paddingTop: number;\n  paddingBottom: number;\n  onToggleRow: (id: string) => void;\n}\n\nexport interface DataTableBodyRowProps<T> extends Pick<\n  DataTableProps<T>,\n  | \"onRowClick\"\n  | \"rowKeyboardActivation\"\n  | \"onRowPointerEnter\"\n  | \"renderRowContextMenu\"\n  | \"rowSizing\"\n> {\n  className?: string;\n  entry: TableRow<T>;\n  index: number;\n  isLastRow: boolean;\n  expanded?: boolean;\n  detailId?: string;\n  rowHeight: number;\n  selectable: boolean;\n  isSelected: boolean;\n  columns: TableColumn<T>[];\n  onToggleRow: (id: string) => void;\n}\n\nexport interface DataTablePagerProps {\n  pagination: DataTablePagination;\n  /** Client paging only: the table owns the page size when the caller does not. */\n  onClientPageSizeChange?: (pageSize: number) => void;\n  /** Rows across all pages, used by client pagination. */\n  rowCount: number;\n}\n",
      "type": "registry:lib",
      "target": "components/table/types/data-table.ts"
    }
  ],
  "type": "registry:component"
}