{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-grid",
  "title": "Data Grid",
  "author": "@fma-ds/ds",
  "description": "A data grid driven by a TanStack Table v9 instance and rendered with react-aria's `Table`, which brings cell keyboard navigation, `aria-sort`, selection, column resizing, row drag and drop and virtualization.\n\n### Table\n\nCreate the table with `useTable({ features: dataGridFeatures, ... })`. The exported features cover sorting, filtering with facets, pagination, selection, row expanding (tree rows from `getSubRows`), row and column pinning, ordering, visibility and sizing, with client-side row models. For server-side data (or to show every row), set `manualSorting`/`manualFiltering`/`manualPagination`.\n\nColumn `meta` takes:\n\n- **`headerTitle`**, **`headerClassName`** and **`cellClassName`**.\n- **`skeleton`**: the cell shown in loading rows.\n- **`autoSize`**: the column takes the remaining width (not measured from content).\n- **`headerColSpan`**: the header spans the next visible columns on its pin side, e.g. \"Quantity\" over the amount and its unit. Covered headers take its accessible name and the last one keeps the resize handle.\n\n### Parts\n\nWrap them in `DataGrid table={table} recordCount={…}`:\n\n- **`DataGridTable`** and **`DataGridPagination`**.\n- **`DataGridColumnHeader`**: sort indicator, plus a menu for pinning, moving and visibility when `tableLayout` enables them.\n- **`DataGridColumnVisibility`**: a column visibility menu.\n- **`DataGridColumnFilter`**: faceted; use `filterFn: \"arrHas\"`.\n- **`DataGridRowSelect`**: a row checkbox, with `enableRowSelection`.\n- **`DataGridRowPin`**: pins to the top, or the bottom with `position=\"bottom\"`.\n- **`DataGridRowActions`**: a row's actions as `DropdownMenuItem`s behind a menu button, named from `rowLabel`. It doesn't trigger `onRowClick`, and with no items it shows a disabled \"no actions\" entry.\n- **`DataGridRowDragHandle`**: with `onRowsReorder`.\n- **`DataGridRowExpand`**: shows `getSubRows` children, indented by depth — or, with `getRowCanExpand` and `DataGridTable`'s `renderExpandedRow`, a full-width details row.\n- **`DataGridSelectionBar`**: shows while rows are selected — the count (announced, with how many are on other pages), your bulk actions as children and \"Clear selection\".\n\n### Rows\n\n- **`onRowClick`**: makes rows actionable (click or Enter).\n- **`onRowHover`**: fires when the pointer enters a row, e.g. to prefetch its details.\n- **`isLoading`**: shows skeleton rows (`meta.skeleton`) or a spinner.\n- **Footer**: `DataGridTable`'s `footerContent` takes footer rows (`DataGridTableFootRow` with `DataGridTableFootRowCell`, `colSpan` to span columns); `tableLayout.footerBackground` tints them.\n\n### Infinite scroll\n\n`DataGridTable`'s `onFetchMore` fires near the end of the loaded rows (`fetchMoreScrollOffset`, in viewport heights), `isFetchingMore` shows a loading row and `hasMore={false}` stops it with `allRowsLoadedMessage`. Works with `isVirtualized`.\n\n### Layout and styling\n\n- **`tableLayout.columnsDraggable`**: lets users drag headers to reorder columns; keyboard users use the header menu's move actions, and pinned columns stay put.\n- **`tableLayout`**: also takes `rowRounded`, `headerBorder` and `columnsResizeMode` (`\"onEnd\"` commits the width on release and draws a line at the pointer while dragging).\n- **`tableClassNames`**: adds classes to parts of the table (`base`, `header`, `headerSticky`, `body`, `bodyRow`, `footer`, `edgeCell`).\n- **Surfaces**: pinned cells and sticky headers paint `--data-grid-surface` (else `--card` inside a `Card`, else `--background`) and carry `data-pinned`/`data-last-col`.\n\n### i18n\n\nThe `i18n` prop on `DataGrid` replaces every built-in string (`{ labels: { sortAscending, rowsPerPage, paginationInfo: ({ from, to, count }) => …, goToPage: (page) => …, … } }`, defaults in `defaultDataGridI18n`). Specific props like `emptyMessage` or the pagination labels still win.",
  "dependencies": [
    "@tanstack/react-table@^9.3.1",
    "cn@^0.5.0",
    "lucide-react@^1.61.0",
    "react-aria-components@^1.22.1"
  ],
  "registryDependencies": [
    "@fma-ds/badge",
    "@fma-ds/button",
    "@fma-ds/checkbox",
    "@fma-ds/dropdown-menu",
    "@fma-ds/input",
    "@fma-ds/popover",
    "@fma-ds/select",
    "@fma-ds/skeleton",
    "@fma-ds/spinner"
  ],
  "files": [
    {
      "path": "src/core/data-grid/data-grid.tsx",
      "content": "\"use client\";\n\nimport {\n  type CellData,\n  columnFacetingFeature,\n  columnFilteringFeature,\n  columnOrderingFeature,\n  columnPinningFeature,\n  columnResizingFeature,\n  columnSizingFeature,\n  columnVisibilityFeature,\n  createExpandedRowModel,\n  createFacetedRowModel,\n  createFacetedUniqueValues,\n  createFilteredRowModel,\n  createPaginatedRowModel,\n  createSortedRowModel,\n  filterFns,\n  flexRender,\n  type RowData,\n  type RowSelectionState,\n  rowExpandingFeature,\n  rowPaginationFeature,\n  rowPinningFeature,\n  rowSelectionFeature,\n  rowSortingFeature,\n  sortFns,\n  type Column as TanstackColumn,\n  type Row as TanstackRow,\n  type Table as TanstackTable,\n  tableFeatures,\n} from \"@tanstack/react-table\";\nimport { cn } from \"cn\";\nimport {\n  ArrowDownIcon,\n  ArrowLeftIcon,\n  ArrowLeftToLineIcon,\n  ArrowRightIcon,\n  ArrowRightToLineIcon,\n  ArrowUpIcon,\n  CheckIcon,\n  ChevronDownIcon,\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  ChevronsUpDownIcon,\n  CirclePlusIcon,\n  EllipsisVerticalIcon,\n  GripVerticalIcon,\n  PinIcon,\n  PinOffIcon,\n} from \"lucide-react\";\nimport * as React from \"react\";\nimport {\n  Button as AriaButton,\n  Cell,\n  Column,\n  ColumnResizer,\n  Dialog,\n  DialogTrigger,\n  type DragAndDropOptions,\n  isTextDropItem,\n  type Key,\n  ListBox,\n  ListBoxItem,\n  ResizableTableContainer,\n  Row,\n  type Selection,\n  type SortDescriptor,\n  Table,\n  TableBody,\n  TableFooter,\n  TableHeader,\n  TableLayout,\n  TableLoadMoreItem,\n  useDrag,\n  useDragAndDrop,\n  useDrop,\n  useLocale,\n  Virtualizer,\n} from \"react-aria-components\";\nimport { Badge } from \"@/core/badge/badge\";\nimport { Button } from \"@/core/button/button\";\nimport { Checkbox } from \"@/core/checkbox/checkbox\";\nimport {\n  DropdownMenu,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/core/dropdown-menu/dropdown-menu\";\nimport { Input } from \"@/core/input/input\";\nimport { Popover } from \"@/core/popover/popover\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/core/select/select\";\nimport { Skeleton } from \"@/core/skeleton/skeleton\";\nimport { Spinner } from \"@/core/spinner/spinner\";\n\ntype DataGridColumnMeta = {\n  /** Label for the header menu, visibility menu and filters. */\n  headerTitle?: string;\n  headerClassName?: string;\n  cellClassName?: string;\n  /** Placeholder shown in this column's cells while loading. */\n  skeleton?: React.ReactNode;\n  /** Takes the remaining width (a `1fr` column). */\n  autoSize?: boolean;\n  /**\n   * The header spans this many columns: this one and the next visible ones on\n   * the same pin side (\"Quantity\" over the amount and its unit). The covered\n   * headers show no title, sort or drag of their own and take this one's\n   * accessible name; the last of them keeps the resize handle.\n   */\n  headerColSpan?: number;\n};\n\n/**\n * The TanStack Table features the grid relies on, with client-side sorting,\n * filtering, faceting and pagination. Create the table with\n * `useTable({ features: dataGridFeatures, ... })`; for server-side data set\n * `manualSorting`/`manualFiltering`/`manualPagination` and pass the page.\n * Column `meta` takes `headerTitle`, `skeleton`, `autoSize`, `headerColSpan`\n * and class names.\n */\nconst dataGridFeatures = tableFeatures({\n  columnFilteringFeature,\n  columnFacetingFeature,\n  columnOrderingFeature,\n  columnPinningFeature,\n  columnSizingFeature,\n  columnResizingFeature,\n  columnVisibilityFeature,\n  rowExpandingFeature,\n  rowPaginationFeature,\n  rowPinningFeature,\n  rowSelectionFeature,\n  rowSortingFeature,\n  // Flattens expanded `getSubRows` children into the rows (tree rows).\n  expandedRowModel: createExpandedRowModel(),\n  filteredRowModel: createFilteredRowModel(),\n  facetedRowModel: createFacetedRowModel(),\n  facetedUniqueValues: createFacetedUniqueValues(),\n  paginatedRowModel: createPaginatedRowModel(),\n  sortedRowModel: createSortedRowModel(),\n  filterFns,\n  sortFns,\n  columnMeta: {} as DataGridColumnMeta,\n});\n\ntype DataGridFeatures = typeof dataGridFeatures;\n\ntype DataGridColumn<TData extends RowData, TValue extends CellData = CellData> = TanstackColumn<\n  DataGridFeatures,\n  TData,\n  TValue\n>;\n\n/** Label for a column: `meta.headerTitle`, a string `columnDef.header`, or `column.id`. */\nfunction getColumnHeaderLabel<TData extends RowData, TValue extends CellData>(\n  column: DataGridColumn<TData, TValue>,\n): string {\n  const { meta, header } = column.columnDef;\n  if (typeof meta?.headerTitle === \"string\") return meta.headerTitle;\n  if (typeof header === \"string\") return header;\n  return column.id;\n}\n\ntype DataGridLayout = {\n  /** Tighter cell padding. */\n  dense?: boolean;\n  cellBorder?: boolean;\n  /** Borders between rows. Defaults to true. */\n  rowBorder?: boolean;\n  /** Rounded row corners. */\n  rowRounded?: boolean;\n  /** Alternating row background. */\n  stripped?: boolean;\n  headerBackground?: boolean;\n  /** Border under the header. Defaults to true. */\n  headerBorder?: boolean;\n  /** Tints the footer rows (`footerContent`). */\n  footerBackground?: boolean;\n  /** Keeps the header visible while the body scrolls (give the grid a height). */\n  headerSticky?: boolean;\n  /** \"fixed\" (default) sizes columns from `size`; \"auto\" lets the content decide. */\n  width?: \"auto\" | \"fixed\";\n  /** Lets users drag column edges to resize (fixed width only). */\n  columnsResizable?: boolean;\n  /**\n   * When a resize reaches the table's `columnSizing`: while dragging\n   * (\"onChange\") or once on release (\"onEnd\"). The column follows the\n   * pointer either way. Defaults to the table's `columnResizeMode`, else \"onChange\".\n   */\n  columnsResizeMode?: \"onChange\" | \"onEnd\";\n  /** Adds pin to start/end to `DataGridColumnHeader` menus. */\n  columnsPinnable?: boolean;\n  /** Adds move left/right to `DataGridColumnHeader` menus. */\n  columnsMovable?: boolean;\n  /**\n   * Lets users drag a header onto another to reorder columns (pointer only;\n   * keyboard users move columns with `columnsMovable`).\n   */\n  columnsDraggable?: boolean;\n  /** Adds a column visibility submenu to `DataGridColumnHeader` menus that ask for it. */\n  columnsVisibility?: boolean;\n};\n\n/** Every built-in string of the grid. Labels that interpolate are functions. */\ntype DataGridI18nLabels = {\n  table: string;\n  loading: string;\n  fetchingMore: string;\n  allRowsLoaded: string;\n  empty: string;\n  sort: string;\n  sortAscending: string;\n  sortDescending: string;\n  pin: string;\n  pinColumnStart: string;\n  pinColumnEnd: string;\n  unpinColumn: (column: string) => string;\n  move: string;\n  moveColumnLeft: string;\n  moveColumnRight: string;\n  columns: string;\n  toggleColumns: string;\n  columnMenu: (column: string) => string;\n  resizeColumn: (column: string) => string;\n  expandRow: string;\n  collapseRow: string;\n  rowDetails: (row: string) => string;\n  pinRow: string;\n  unpinRow: string;\n  rowActions: (row?: string) => string;\n  noRowActions: string;\n  filter: string;\n  filterSearch: (title: string) => string;\n  filterNoResults: string;\n  filterClear: string;\n  filterSelected: (count: number) => string;\n  selectedRows: (count: number) => string;\n  selectedOnOtherPages: (count: number) => string;\n  clearSelection: string;\n  pagination: string;\n  rowsPerPage: string;\n  paginationInfo: (info: { from: number; to: number; count: number }) => string;\n  previousPage: string;\n  nextPage: string;\n  goToPage: (page: number) => string;\n  ellipsis: string;\n};\n\ntype DataGridI18nConfig = { labels: DataGridI18nLabels };\n\n/** Partial overrides, merged over the defaults per section. */\ntype DataGridI18nOverrides = { labels?: Partial<DataGridI18nLabels> };\n\nconst defaultDataGridI18n: DataGridI18nConfig = {\n  labels: {\n    table: \"Data grid\",\n    loading: \"Loading...\",\n    fetchingMore: \"Loading more...\",\n    allRowsLoaded: \"All records loaded\",\n    empty: \"No data available\",\n    sort: \"Sort\",\n    sortAscending: \"Ascending\",\n    sortDescending: \"Descending\",\n    pin: \"Pin\",\n    pinColumnStart: \"Pin to start\",\n    pinColumnEnd: \"Pin to end\",\n    unpinColumn: (column) => `Unpin ${column} column`,\n    move: \"Move\",\n    moveColumnLeft: \"Move left\",\n    moveColumnRight: \"Move right\",\n    columns: \"Columns\",\n    toggleColumns: \"Toggle columns\",\n    columnMenu: (column) => `${column} column`,\n    resizeColumn: (column) => `Resize ${column}`,\n    expandRow: \"Expand row\",\n    collapseRow: \"Collapse row\",\n    rowDetails: (row) => `${row} details`,\n    pinRow: \"Pin row\",\n    unpinRow: \"Unpin row\",\n    rowActions: (row) => (row ? `Actions for ${row}` : \"Row actions\"),\n    noRowActions: \"No actions available\",\n    filter: \"Filter\",\n    filterSearch: (title) => `Search ${title}`,\n    filterNoResults: \"No results found.\",\n    filterClear: \"Clear filters\",\n    filterSelected: (count) => `${count} selected`,\n    selectedRows: (count) => `${count} selected`,\n    selectedOnOtherPages: (count) => `(${count} on other pages)`,\n    clearSelection: \"Clear selection\",\n    pagination: \"Pagination\",\n    rowsPerPage: \"Rows per page\",\n    paginationInfo: ({ from, to, count }) => `${from} - ${to} of ${count}`,\n    previousPage: \"Go to previous page\",\n    nextPage: \"Go to next page\",\n    goToPage: (page) => `Page ${page}`,\n    ellipsis: \"...\",\n  },\n};\n\n/** Merges overrides over the defaults; untouched keys keep their default. */\nfunction mergeDataGridI18n(overrides?: DataGridI18nOverrides): DataGridI18nConfig {\n  return { labels: { ...defaultDataGridI18n.labels, ...overrides?.labels } };\n}\n\n/** Extra classes for parts of the table. */\ntype DataGridTableClassNames = {\n  /** The `<table>`. */\n  base?: string;\n  /** The `<thead>` (its row through `[&>tr]:…`). */\n  header?: string;\n  /** Added to the `<thead>` with `headerSticky`. */\n  headerSticky?: string;\n  /** The `<tbody>`. */\n  body?: string;\n  /** Each data row. */\n  bodyRow?: string;\n  /** The `<tfoot>`. */\n  footer?: string;\n  /** The first and last cell of each header and data row. */\n  edgeCell?: string;\n};\n\ntype DataGridProps<TData extends RowData> = {\n  /** A table created with `useTable({ features: dataGridFeatures, ... })`. */\n  table: TanstackTable<DataGridFeatures, TData>;\n  /** Total rows across pages (the server's total with manual pagination). */\n  recordCount: number;\n  isLoading?: boolean;\n  /** \"skeleton\" (default) shows placeholder rows; \"spinner\" overlays the current rows. */\n  loadingMode?: \"skeleton\" | \"spinner\";\n  /** Shown beside the spinner in the \"spinner\" loading mode. Defaults to \"Loading...\". */\n  loadingMessage?: React.ReactNode;\n  /** Shown while `DataGridTable` fetches more rows (`onFetchMore`). Defaults to `loadingMessage`. */\n  fetchingMoreMessage?: React.ReactNode;\n  /** Shown under the rows once `hasMore` is false. Defaults to \"All records loaded\". */\n  allRowsLoadedMessage?: React.ReactNode;\n  /** Defaults to \"No data available\". */\n  emptyMessage?: React.ReactNode;\n  /**\n   * Overrides for the built-in strings (menus, aria labels, pagination,\n   * filter). Specific props such as `loadingMessage`, `emptyMessage` or the\n   * pagination label props win over their `i18n` counterpart.\n   */\n  i18n?: DataGridI18nOverrides;\n  /** Makes rows actionable: pressing one (or Enter on it) calls this. */\n  onRowClick?: (row: TData) => void;\n  /** Fires when the pointer enters a row, e.g. to prefetch its details. */\n  onRowHover?: (row: TData) => void;\n  tableLayout?: DataGridLayout;\n  tableClassNames?: DataGridTableClassNames;\n  className?: string;\n  children?: React.ReactNode;\n};\n\ntype DataGridContextValue = Omit<DataGridProps<RowData>, \"table\" | \"children\" | \"i18n\"> & {\n  table: TanstackTable<DataGridFeatures, RowData>;\n  layout: DataGridLayout;\n  i18n: DataGridI18nConfig;\n};\n\nconst DataGridContext = React.createContext<DataGridContextValue | null>(null);\n\nfunction useDataGrid<TData extends RowData = RowData>() {\n  const context = React.useContext(DataGridContext);\n  if (!context) throw new Error(\"useDataGrid must be used within a DataGrid\");\n  return context as Omit<DataGridContextValue, \"table\"> & {\n    table: TanstackTable<DataGridFeatures, TData>;\n  };\n}\n\n/** The grid's labels; the defaults outside a `DataGrid`. */\nfunction useDataGridLabels(): DataGridI18nLabels {\n  return (React.useContext(DataGridContext)?.i18n ?? defaultDataGridI18n).labels;\n}\n\n/** Provides the TanStack `table` to the grid parts (table, pagination, headers). */\nfunction DataGrid<TData extends RowData>({\n  table,\n  tableLayout,\n  className,\n  children,\n  i18n,\n  ...props\n}: DataGridProps<TData>) {\n  // Rebuilt every render on purpose: the table instance is stable while its\n  // state changes, and the parts must re-render with it.\n  const value: DataGridContextValue = {\n    // The context is untyped; useDataGrid<TData>() restores the row type.\n    ...(props as Omit<DataGridProps<RowData>, \"table\" | \"children\" | \"i18n\">),\n    table: table as TanstackTable<DataGridFeatures, RowData>,\n    layout: { rowBorder: true, headerBorder: true, width: \"fixed\", ...tableLayout },\n    i18n: mergeDataGridI18n(i18n),\n  };\n  return (\n    <DataGridContext.Provider value={value}>\n      <div data-slot=\"data-grid\" className={cn(\"flex w-full flex-col gap-2.5\", className)}>\n        {children}\n      </div>\n    </DataGridContext.Provider>\n  );\n}\n\n/** Frames the table: border, rounded corners and clipping. */\nfunction DataGridContainer({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"data-grid-container\"\n      className={cn(\"w-full overflow-hidden rounded-lg border\", className)}\n      {...props}\n    />\n  );\n}\n\n// Logical insets, so pinned columns stick to the correct side in RTL too.\nfunction pinningStyle<TData extends RowData>(\n  column: DataGridColumn<TData>,\n  zIndex = 1,\n): React.CSSProperties | undefined {\n  const pinned = column.getIsPinned();\n  if (!pinned) return undefined;\n  return {\n    position: \"sticky\",\n    zIndex,\n    ...(pinned === \"start\"\n      ? { insetInlineStart: column.getStart(\"start\") }\n      : { insetInlineEnd: column.getAfter(\"end\") }),\n  };\n}\n\n/**\n * Pinning hooks on header and body cells: `data-pinned` names the side and\n * `data-last-col` marks the group's inner edge (the last start-pinned or\n * first end-pinned column), which carries the divider.\n */\nfunction pinningAttributes<TData extends RowData>(column: DataGridColumn<TData>) {\n  const pinned = column.getIsPinned();\n  if (!pinned) return {};\n  const isInner =\n    pinned === \"start\" ? column.getIsLastColumn(\"start\") : column.getIsFirstColumn(\"end\");\n  return { \"data-pinned\": pinned, \"data-last-col\": isInner ? pinned : undefined };\n}\n\n/**\n * `meta.headerColSpan`: the columns each spanning header covers, by owner id.\n * A span takes the next visible columns on the owner's pin side and ends at\n * that side's edge, so a sticky group never covers a scrolling one.\n */\nfunction getHeaderSpans<TData extends RowData>(families: DataGridColumn<TData>[][]) {\n  const spans = new Map<string, DataGridColumn<TData>[]>();\n  for (const family of families) {\n    for (let index = 0; index < family.length; index++) {\n      const owner = family[index] as DataGridColumn<TData>;\n      const span = Math.floor(owner.columnDef.meta?.headerColSpan ?? 1);\n      if (span < 2) continue;\n      const covered = family.slice(index + 1, index + span);\n      if (!covered.length) continue;\n      spans.set(owner.id, covered);\n      index += covered.length;\n    }\n  }\n  return spans;\n}\n\n/**\n * Stretches each spanning header's content over the columns it covers. The\n * table has one header cell per column, so the owner's content overflows into\n * the covered cells; their widths are measured, which holds for `1fr` columns\n * and follows a resize.\n */\nfunction useHeaderSpanWidths(\n  containerRef: React.RefObject<HTMLDivElement | null>,\n  spans: Map<string, { id: string }[]>,\n) {\n  const key = [...spans]\n    .map(([id, covered]) => [id, ...covered.map((c) => c.id)].join(\",\"))\n    .join(\";\");\n  React.useLayoutEffect(() => {\n    const container = containerRef.current;\n    if (!container || !key) return;\n    const headerOf = (id: string) =>\n      container.querySelector<HTMLElement>(`[data-column-id=\"${CSS.escape(id)}\"]`);\n    const update = () => {\n      // Queried on each pass: the collection mounts its header cells after\n      // this effect first runs. Observing a cell twice is a no-op.\n      for (const group of key.split(\";\")) {\n        const [owner, ...covered] = group.split(\",\").map(headerOf);\n        const extra = covered.reduce((sum, cell) => sum + (cell?.offsetWidth ?? 0), 0);\n        owner?.style.setProperty(\"--data-grid-header-span\", `${extra}px`);\n        for (const cell of [owner, ...covered]) if (cell) observer.observe(cell);\n      }\n    };\n    const observer = new ResizeObserver(update);\n    observer.observe(container);\n    update();\n    return () => observer.disconnect();\n  }, [containerRef, key]);\n}\n\n/**\n * The opaque surface behind pinned cells and sticky headers:\n * `--data-grid-surface` when set on an ancestor, else `--card` inside a Card,\n * else `--background`. Raw tokens: `--color-*` resolve at :root, so they'd\n * stay light under a scoped `.dark` subtree.\n */\nconst pinnedDivider = \"data-[last-col=end]:border-s data-[last-col=start]:border-e\";\n\nconst surfaceBackground =\n  \"bg-[color:var(--data-grid-surface,var(--data-grid-card-surface,var(--background)))]\";\n\ntype DataGridTableProps<TData extends RowData = RowData> = {\n  \"aria-label\"?: string;\n  className?: string;\n  /**\n   * Enables row reordering by drag and drop (rows need a `DataGridRowDragHandle`).\n   * Receives react-aria's reorder event: the dragged keys (row ids) and the target.\n   */\n  onRowsReorder?: DragAndDropOptions[\"onReorder\"];\n  /** Renders only the visible rows. Give the table a fixed height through `className`. */\n  isVirtualized?: boolean;\n  /** Row height for virtualization, in pixels (an estimate when rows can expand). */\n  rowHeight?: number;\n  /**\n   * Content of a full-width row shown under each expanded row. Expand rows\n   * with `DataGridRowExpand` and make them expandable with `getRowCanExpand`.\n   */\n  renderExpandedRow?: (row: TanstackRow<DataGridFeatures, TData>) => React.ReactNode;\n  /**\n   * Footer rows (`DataGridTableFootRow`), rendered in the table's footer\n   * (`<tfoot>`), e.g. totals.\n   */\n  footerContent?: React.ReactNode;\n  /**\n   * Infinite scroll: called when the user scrolls near the end of the loaded\n   * rows (within one viewport height, see `fetchMoreScrollOffset`). Append\n   * the next rows to `data`; give the table a height through `className`.\n   */\n  onFetchMore?: () => void;\n  /** Whether `onFetchMore` is loading; shows a loading row under the rows. */\n  isFetchingMore?: boolean;\n  /** False once every row is loaded: stops `onFetchMore` and shows `allRowsLoadedMessage`. */\n  hasMore?: boolean;\n  /**\n   * How close to the end `onFetchMore` fires, as a fraction of the viewport\n   * height (1 = within one viewport). Defaults to 1.\n   */\n  fetchMoreScrollOffset?: number;\n};\n\n// Key suffix of the full-width row rendered under an expanded row.\nconst EXPANDED = \":expanded\";\n// Key of the end-of-list row under infinite scroll.\nconst ALL_LOADED = \"data-grid:all-loaded\";\n\n/**\n * Renders the grid with react-aria's `Table`: keyboard navigation between\n * cells, `aria-sort`, selection and resizing come from react-aria while\n * TanStack holds the data model.\n */\nfunction DataGridTable<TData extends RowData = RowData>({\n  \"aria-label\": ariaLabelProp,\n  className,\n  onRowsReorder,\n  isVirtualized,\n  rowHeight = 40,\n  renderExpandedRow,\n  footerContent,\n  onFetchMore,\n  isFetchingMore,\n  hasMore,\n  fetchMoreScrollOffset,\n}: DataGridTableProps<TData>) {\n  const {\n    table,\n    layout,\n    isLoading,\n    loadingMode = \"skeleton\",\n    loadingMessage,\n    fetchingMoreMessage,\n    allRowsLoadedMessage,\n    emptyMessage,\n    onRowClick,\n    onRowHover,\n    tableClassNames: classNames = {},\n    i18n: { labels },\n  } = useDataGrid();\n  const ariaLabel = ariaLabelProp ?? labels.table;\n  const state = table.store.state;\n  const families = [\n    table.getStartVisibleLeafColumns(),\n    table.getCenterVisibleLeafColumns(),\n    table.getEndVisibleLeafColumns(),\n  ];\n  const columns = families.flat();\n  const spans = getHeaderSpans(families);\n  // Covered column id → the column whose header spans it.\n  const spannedBy = new Map(\n    [...spans].flatMap(([id, covered]) => covered.map((column) => [column.id, id] as const)),\n  );\n  const columnsById = new Map(columns.map((column) => [column.id, column]));\n  // The first column holding data names each row (not a checkbox or handle).\n  const rowHeaderId = (columns.find((column) => column.accessorFn) ?? columns[0])?.id;\n  const headers = new Map(\n    table.getFlatHeaders().map((header) => [header.column.id, header] as const),\n  );\n  const topRows = table.getTopRows();\n  const bottomRows = table.getBottomRows();\n  const rows = [...topRows, ...table.getCenterRows(), ...bottomRows];\n  // The seam between pinned and unpinned rows.\n  const topBoundaryId = topRows.at(-1)?.id;\n  const bottomBoundaryId = bottomRows[0]?.id;\n  const rowsById = new Map(rows.map((row) => [row.id, row]));\n  const showSkeleton = isLoading && loadingMode === \"skeleton\";\n  const skeletonIds = Array.from(\n    { length: state.pagination.pageSize || 5 },\n    (_, index) => `skeleton-${index}`,\n  );\n\n  const { enableRowSelection } = table.options;\n  const isSelectable = enableRowSelection === true || typeof enableRowSelection === \"function\";\n  const sorting = state.sorting[0];\n  const sortDescriptor: SortDescriptor | undefined = sorting && {\n    column: sorting.id,\n    direction: sorting.desc ? \"descending\" : \"ascending\",\n  };\n\n  const { dragAndDropHooks } = useDragAndDrop({\n    getItems: (keys) =>\n      [...keys]\n        .filter((key) => !String(key).endsWith(EXPANDED))\n        .map((key) => ({ \"text/plain\": String(key) })),\n    onReorder: onRowsReorder,\n    isDisabled: !onRowsReorder,\n  });\n\n  const fixed = layout.width === \"fixed\";\n  const overlapsRows = layout.headerSticky || isVirtualized;\n  const cellPadding = layout.dense ? \"px-2.5 py-1.5\" : \"px-4 py-2.5\";\n\n  const onSelectionChange = (keys: Selection) => {\n    const pageIds = new Set(rows.map((row) => row.id));\n    const selected =\n      keys === \"all\" ? [...pageIds] : [...keys].map(String).filter((id) => pageIds.has(id));\n    // Keep selections made on other pages.\n    const next: RowSelectionState = {};\n    for (const id of Object.keys(state.rowSelection)) {\n      if (!pageIds.has(id)) next[id] = true;\n    }\n    for (const id of selected) next[id] = true;\n    table.setRowSelection(next);\n  };\n\n  const resizeMode = layout.columnsResizeMode ?? table.options.columnResizeMode ?? \"onChange\";\n  const commitSizing = (widths: Map<Key, number | string>) => {\n    const sizing: Record<string, number> = {};\n    for (const [id, width] of widths) if (typeof width === \"number\") sizing[String(id)] = width;\n    table.setColumnSizing((old) => ({ ...old, ...sizing }));\n  };\n  // In \"onEnd\" mode the column keeps its width until release, so a line at\n  // the pointer shows where the edge will land. Positioned imperatively: the\n  // drag fires at pointer rate, and a render per move would redraw every row.\n  const containerRef = React.useRef<HTMLDivElement>(null);\n  const indicatorRef = React.useRef<HTMLDivElement>(null);\n  // The column whose resizer the user grabbed (pointer or keyboard).\n  const resizingColumnRef = React.useRef<string | null>(null);\n  const showResizeIndicator = (widths: Map<Key, number | string>) => {\n    const container = containerRef.current;\n    const indicator = indicatorRef.current;\n    const index = columns.findIndex((column) => column.id === resizingColumnRef.current);\n    const header = container?.querySelectorAll<HTMLElement>(\"[role=columnheader]\")[index];\n    const width = widths.get(resizingColumnRef.current ?? \"\");\n    if (!container || !indicator || !header || typeof width !== \"number\") return;\n    const box = container.getBoundingClientRect();\n    const cell = header.getBoundingClientRect();\n    const isRtl = getComputedStyle(container).direction === \"rtl\";\n    indicator.style.left = `${isRtl ? cell.right - width - box.left : cell.left + width - box.left}px`;\n    indicator.hidden = false;\n  };\n  const endResize = (widths: Map<Key, number | string>) => {\n    if (indicatorRef.current) indicatorRef.current.hidden = true;\n    commitSizing(widths);\n  };\n  useHeaderSpanWidths(containerRef, spans);\n\n  let content = (\n    <Table\n      aria-label={ariaLabel}\n      data-slot=\"data-grid-table\"\n      sortDescriptor={sortDescriptor}\n      onSortChange={(descriptor) =>\n        table.setSorting([\n          { id: String(descriptor.column), desc: descriptor.direction === \"descending\" },\n        ])\n      }\n      selectionMode={isSelectable && !showSkeleton ? \"multiple\" : \"none\"}\n      selectedKeys={Object.keys(state.rowSelection)}\n      onSelectionChange={onSelectionChange}\n      disabledKeys={[\n        ...rows.filter((row) => isSelectable && !row.getCanSelect()).map((row) => row.id),\n        ...rows.filter((row) => row.getIsExpanded()).map((row) => row.id + EXPANDED),\n        ALL_LOADED,\n      ]}\n      // Disabled rows (unselectable ones, expanded panels) stay focusable.\n      disabledBehavior=\"selection\"\n      onRowAction={\n        onRowClick && !showSkeleton\n          ? (key) => {\n              const row = rowsById.get(String(key));\n              if (row) onRowClick(row.original);\n            }\n          : undefined\n      }\n      dragAndDropHooks={onRowsReorder ? dragAndDropHooks : undefined}\n      className={cn(\n        \"w-full caption-bottom border-separate border-spacing-0 text-sm outline-none\",\n        fixed && \"table-fixed\",\n        classNames.base,\n      )}\n    >\n      <TableHeader\n        className={cn(\n          layout.headerSticky && \"sticky top-0\",\n          overlapsRows && \"z-10\",\n          classNames.header,\n          layout.headerSticky && classNames.headerSticky,\n        )}\n      >\n        {columns.map((column, index) => {\n          const header = headers.get(column.id);\n          const width = column.columnDef.meta?.autoSize ? \"1fr\" : column.getSize();\n          const ownerId = spannedBy.get(column.id);\n          const owner = ownerId ? columnsById.get(ownerId) : undefined;\n          const isSpanOwner = spans.has(column.id);\n          // The span reads as one cell: no divider inside it.\n          const joinsNext =\n            isSpanOwner || (!!ownerId && spans.get(ownerId)?.at(-1)?.id !== column.id);\n          const isSpanned = isSpanOwner || !!owner;\n          if (owner) {\n            // A covered header: the owner's title shows over it, and its name\n            // repeats the owner's, so each column is still announced.\n            const ownerLabel = getColumnHeaderLabel(owner);\n            return (\n              <Column\n                key={column.id}\n                id={column.id}\n                textValue={ownerLabel}\n                isRowHeader={column.id === rowHeaderId}\n                width={fixed ? width : undefined}\n                minWidth={fixed ? column.columnDef.minSize : undefined}\n                style={pinningStyle(column, 2)}\n                {...pinningAttributes(column)}\n                data-column-id={column.id}\n                data-header-covered=\"\"\n                className={cn(\n                  \"group/column relative h-10 outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset\",\n                  isVirtualized && \"flex items-center\",\n                  layout.dense ? \"px-2.5\" : \"px-4\",\n                  layout.headerBorder && \"border-b\",\n                  layout.cellBorder && !joinsNext && \"border-e last:border-e-0\",\n                  pinnedDivider,\n                  index === columns.length - 1 && classNames.edgeCell,\n                  layout.headerBackground\n                    ? \"bg-[color-mix(in_oklab,var(--muted)_40%,var(--data-grid-surface,var(--data-grid-card-surface,var(--background))))]\"\n                    : (overlapsRows || column.getIsPinned()) && surfaceBackground,\n                  column.columnDef.meta?.headerClassName,\n                )}\n              >\n                <span className=\"sr-only\">{ownerLabel}</span>\n                {!joinsNext && layout.columnsResizable && fixed && column.getCanResize() && (\n                  <div\n                    onPointerDownCapture={() => {\n                      resizingColumnRef.current = column.id;\n                    }}\n                    onKeyDownCapture={() => {\n                      resizingColumnRef.current = column.id;\n                    }}\n                  >\n                    <ColumnResizer\n                      aria-label={labels.resizeColumn(getColumnHeaderLabel(column))}\n                      className=\"absolute inset-y-0 -end-px z-10 w-1.5 cursor-col-resize touch-none bg-transparent outline-none data-focus-visible:bg-ring data-hovered:bg-border data-resizing:bg-ring\"\n                    />\n                  </div>\n                )}\n              </Column>\n            );\n          }\n          return (\n            <Column\n              key={column.id}\n              id={column.id}\n              isRowHeader={column.id === rowHeaderId}\n              allowsSorting={column.getCanSort() && !showSkeleton}\n              width={fixed ? width : undefined}\n              minWidth={fixed ? column.columnDef.minSize : undefined}\n              // Header cells stack above body cells; an owner above the cells it covers.\n              style={\n                pinningStyle(column, isSpanOwner ? 3 : 2) ??\n                (isSpanOwner ? { zIndex: 1 } : undefined)\n              }\n              {...pinningAttributes(column)}\n              data-column-id={column.id}\n              data-header-span={isSpanOwner ? (spans.get(column.id)?.length ?? 0) + 1 : undefined}\n              className={cn(\n                \"group/column relative h-10 text-left align-middle font-medium whitespace-nowrap text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset\",\n                isVirtualized && \"flex items-center\",\n                layout.dense ? \"px-2.5\" : \"px-4\",\n                layout.headerBorder && \"border-b\",\n                layout.cellBorder && !joinsNext && \"border-e last:border-e-0\",\n                pinnedDivider,\n                (index === 0 || index === columns.length - 1) && classNames.edgeCell,\n                // Opaque backgrounds on the cells: rows scroll under a sticky or virtualized\n                // header, and the virtualized header row group paints no background.\n                // Mixes the raw tokens: `--color-*` resolve at :root, so they'd stay\n                // light under a `.dark` subtree.\n                layout.headerBackground\n                  ? \"bg-[color-mix(in_oklab,var(--muted)_40%,var(--data-grid-surface,var(--data-grid-card-surface,var(--background))))]\"\n                  : (overlapsRows || column.getIsPinned()) && surfaceBackground,\n                column.columnDef.meta?.headerClassName,\n              )}\n            >\n              <div\n                className={cn(\n                  \"flex items-center gap-1\",\n                  isSpanOwner && \"w-[calc(100%+var(--data-grid-header-span,0px))]\",\n                )}\n                onPointerDownCapture={(event) => {\n                  if ((event.target as Element).closest(\"[data-resizable-direction]\"))\n                    resizingColumnRef.current = column.id;\n                }}\n                onKeyDownCapture={(event) => {\n                  if ((event.target as Element).closest(\"[data-resizable-direction]\"))\n                    resizingColumnRef.current = column.id;\n                }}\n              >\n                {/* Pinned columns keep their place: no drag source, no drop target. */}\n                {layout.columnsDraggable && !showSkeleton && !column.getIsPinned() && !isSpanned ? (\n                  <ColumnDragArea column={column} dense={layout.dense}>\n                    {header && !header.isPlaceholder\n                      ? flexRender(column.columnDef.header, header.getContext())\n                      : null}\n                  </ColumnDragArea>\n                ) : (\n                  <div className=\"min-w-0 flex-1\">\n                    {header && !header.isPlaceholder\n                      ? flexRender(column.columnDef.header, header.getContext())\n                      : null}\n                  </div>\n                )}\n                {layout.columnsResizable && fixed && column.getCanResize() && !isSpanOwner && (\n                  <ColumnResizer\n                    aria-label={labels.resizeColumn(getColumnHeaderLabel(column))}\n                    className=\"absolute inset-y-0 -end-px z-10 w-1.5 cursor-col-resize touch-none bg-transparent outline-none data-focus-visible:bg-ring data-hovered:bg-border data-resizing:bg-ring\"\n                  />\n                )}\n              </div>\n            </Column>\n          );\n        })}\n      </TableHeader>\n      <TableBody\n        className={classNames.body}\n        renderEmptyState={() => (\n          <div className=\"py-10 text-center text-sm text-muted-foreground\">\n            {emptyMessage ?? labels.empty}\n          </div>\n        )}\n      >\n        {showSkeleton\n          ? skeletonIds.map((id) => (\n              <Row key={id} id={id} className={rowClassName(layout, isVirtualized)}>\n                {columns.map((column) => (\n                  <Cell\n                    key={column.id}\n                    className={cn(cellPadding, cellClassName(layout, isVirtualized))}\n                  >\n                    {column.columnDef.meta?.skeleton ?? <Skeleton className=\"h-4 w-full\" />}\n                  </Cell>\n                ))}\n              </Row>\n            ))\n          : rows.flatMap((row) => [\n              <Row\n                key={row.id}\n                id={row.id}\n                // Names the row in drag-and-drop and selection announcements.\n                textValue={String((rowHeaderId && row.getValue(rowHeaderId)) ?? row.id)}\n                data-row-id={row.id}\n                onHoverStart={onRowHover ? () => onRowHover(row.original) : undefined}\n                data-depth={row.depth || undefined}\n                data-row-pinned={row.getIsPinned() || undefined}\n                data-row-pinned-boundary={\n                  row.id === topBoundaryId\n                    ? \"top\"\n                    : row.id === bottomBoundaryId\n                      ? \"bottom\"\n                      : undefined\n                }\n                className={cn(\n                  rowClassName(layout, isVirtualized),\n                  onRowClick && \"cursor-pointer\",\n                  \"data-row-pinned:bg-muted/40\",\n                  classNames.bodyRow,\n                )}\n              >\n                {[\n                  ...row.getStartVisibleCells(),\n                  ...row.getCenterVisibleCells(),\n                  ...row.getEndVisibleCells(),\n                ].map((cell, index, cells) => (\n                  <Cell\n                    key={cell.id}\n                    style={pinningStyle(cell.column)}\n                    {...pinningAttributes(cell.column)}\n                    className={cn(\n                      cellPadding,\n                      cellClassName(layout, isVirtualized),\n                      pinnedDivider,\n                      cell.column.getIsPinned() && surfaceBackground,\n                      (index === 0 || index === cells.length - 1) && classNames.edgeCell,\n                      cell.column.columnDef.meta?.cellClassName,\n                    )}\n                  >\n                    {flexRender(cell.column.columnDef.cell, cell.getContext())}\n                  </Cell>\n                ))}\n              </Row>,\n              ...(renderExpandedRow && row.getIsExpanded()\n                ? [\n                    <Row\n                      key={row.id + EXPANDED}\n                      id={row.id + EXPANDED}\n                      textValue={labels.rowDetails(\n                        String((rowHeaderId && row.getValue(rowHeaderId)) ?? row.id),\n                      )}\n                      data-slot=\"data-grid-expanded-row\"\n                      className={rowClassName({ ...layout, stripped: false }, isVirtualized)}\n                    >\n                      <Cell\n                        colSpan={columns.length}\n                        className={cn(\n                          cellPadding,\n                          cellClassName(layout, isVirtualized),\n                          \"bg-muted/20\",\n                        )}\n                      >\n                        {renderExpandedRow(row as TanstackRow<DataGridFeatures, TData>)}\n                      </Cell>\n                    </Row>,\n                  ]\n                : []),\n            ])}\n        {onFetchMore && !showSkeleton && (\n          <TableLoadMoreItem\n            data-slot=\"data-grid-fetching-more\"\n            onLoadMore={hasMore === false ? undefined : onFetchMore}\n            isLoading={!!isFetchingMore && hasMore !== false}\n            scrollOffset={fetchMoreScrollOffset}\n            className={cn(isVirtualized && \"flex items-center justify-center\")}\n          >\n            <div\n              role=\"status\"\n              className=\"flex items-center justify-center gap-2 py-3 text-sm text-muted-foreground\"\n            >\n              <Spinner className=\"size-4\" role=\"presentation\" aria-hidden=\"true\" />\n              {fetchingMoreMessage ?? loadingMessage ?? labels.fetchingMore}\n            </div>\n          </TableLoadMoreItem>\n        )}\n        {onFetchMore && hasMore === false && !showSkeleton && rows.length > 0 && (\n          <Row\n            id={ALL_LOADED}\n            textValue={labels.allRowsLoaded}\n            data-slot=\"data-grid-all-loaded\"\n            className={rowClassName({ ...layout, stripped: false }, isVirtualized)}\n          >\n            <Cell\n              colSpan={columns.length}\n              className={cn(\n                cellPadding,\n                cellClassName(layout, isVirtualized),\n                \"justify-center text-center text-sm text-muted-foreground\",\n              )}\n            >\n              {allRowsLoadedMessage ?? labels.allRowsLoaded}\n            </Cell>\n          </Row>\n        )}\n      </TableBody>\n      {footerContent ? <DataGridTableFoot>{footerContent}</DataGridTableFoot> : null}\n    </Table>\n  );\n\n  if (isVirtualized) {\n    content = (\n      <Virtualizer\n        layout={TableLayout}\n        layoutOptions={\n          // Expanded panels are taller than rows: measure rows instead of fixing their height.\n          renderExpandedRow\n            ? { estimatedRowHeight: rowHeight, headingHeight: rowHeight }\n            : { rowHeight, headingHeight: rowHeight }\n        }\n      >\n        {content}\n      </Virtualizer>\n    );\n  }\n\n  return (\n    <div\n      ref={containerRef}\n      data-slot=\"data-grid-table-container\"\n      aria-busy={isLoading || undefined}\n      // Pinned cells and sticky headers follow a Card's surface.\n      className=\"relative [[data-slot=card]_&]:[--data-grid-card-surface:var(--card)]\"\n    >\n      <div\n        ref={indicatorRef}\n        hidden\n        aria-hidden=\"true\"\n        data-slot=\"data-grid-table-resize-indicator\"\n        className=\"pointer-events-none absolute inset-y-0 z-30 w-0.5 -translate-x-1/2 bg-primary\"\n      />\n      <ResizableTableContainer\n        data-slot=\"data-grid-table-viewport\"\n        onResize={resizeMode === \"onChange\" ? commitSizing : showResizeIndicator}\n        onResizeEnd={resizeMode === \"onEnd\" ? endResize : undefined}\n        className={cn(\"relative w-full overflow-auto\", className)}\n      >\n        {content}\n      </ResizableTableContainer>\n      {isLoading && loadingMode === \"spinner\" && (\n        <div\n          data-slot=\"data-grid-loading\"\n          role=\"status\"\n          className=\"absolute inset-0 z-20 flex items-center justify-center gap-2 bg-background/60 text-sm text-muted-foreground\"\n        >\n          <Spinner className=\"size-5\" role=\"presentation\" aria-hidden=\"true\" />\n          {loadingMessage ?? labels.loading}\n        </div>\n      )}\n    </div>\n  );\n}\n\n// Native table rows can't draw borders (border-separate), so cells do. A\n// virtualized table renders positioned divs instead: the row draws the border\n// and cells center their content with flex.\nfunction rowClassName(layout: DataGridLayout, isVirtualized?: boolean) {\n  return cn(\n    \"outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset data-hovered:bg-accent/50 data-selected:bg-accent data-selected:text-accent-foreground data-dragging:opacity-50 data-drop-target:bg-accent\",\n    layout.stripped && \"odd:bg-muted/30\",\n    isVirtualized && layout.rowRounded && \"rounded-lg\",\n    // Virtualized rows are zero-height boxes (cells are positioned), so this\n    // border draws at each row's top: skip the first body row (rowindex 2,\n    // after the header), which sits right under the header's own border.\n    isVirtualized && layout.rowBorder && \"border-b aria-[rowindex=2]:border-b-0\",\n  );\n}\n\nfunction cellClassName(layout: DataGridLayout, isVirtualized?: boolean) {\n  return cn(\n    \"align-middle outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset\",\n    isVirtualized && \"flex items-center\",\n    !isVirtualized && layout.rowBorder && \"border-b [tr:last-child>&]:border-b-0\",\n    layout.cellBorder && \"border-e last:border-e-0\",\n    !isVirtualized && layout.rowRounded && \"first:rounded-s-lg last:rounded-e-lg\",\n  );\n}\n\n/** The table's footer (`<tfoot>`); `footerContent` on `DataGridTable` renders one for you. */\nfunction DataGridTableFoot({ className, ...props }: React.ComponentProps<typeof TableFooter>) {\n  const { tableClassNames } = useDataGrid();\n  return (\n    <TableFooter\n      data-slot=\"data-grid-table-foot\"\n      className={cn(\"font-medium\", tableClassNames?.footer, className)}\n      {...props}\n    />\n  );\n}\n\n/** A footer row, inside `footerContent`. */\nfunction DataGridTableFootRow({ className, ...props }: React.ComponentProps<typeof Row>) {\n  const { layout } = useDataGrid();\n  return (\n    <Row\n      data-slot=\"data-grid-table-foot-row\"\n      className={cn(\n        \"outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset\",\n        layout.footerBackground && \"bg-muted/40\",\n        className as string,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** A footer cell; `colSpan` spans it over several columns. */\nfunction DataGridTableFootRowCell({ className, ...props }: React.ComponentProps<typeof Cell>) {\n  const { layout } = useDataGrid();\n  return (\n    <Cell\n      data-slot=\"data-grid-table-foot-cell\"\n      className={cn(\n        \"align-middle outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset\",\n        layout.dense ? \"px-2.5 py-1.5\" : \"px-4 py-2.5\",\n        // The body's last row draws no bottom border: the footer's first row\n        // draws the seam.\n        \"[tr:first-child>&]:border-t\",\n        layout.rowBorder && \"border-b [tr:last-child>&]:border-b-0\",\n        layout.cellBorder && \"border-e last:border-e-0\",\n        className as string,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst COLUMN_DRAG_TYPE = \"application/x-fma-data-grid-column\";\n\n/**\n * A header's drag source and drop target for `columnsDraggable`: dropping a\n * header on another moves it before or after that column, by pointer side.\n */\nfunction ColumnDragArea({\n  column,\n  dense,\n  children,\n}: {\n  column: DataGridColumn<RowData>;\n  dense?: boolean;\n  children: React.ReactNode;\n}) {\n  const { table } = useDataGrid();\n  const { direction } = useLocale();\n  const ref = React.useRef<HTMLDivElement>(null);\n  const [side, setSide] = React.useState<\"before\" | \"after\" | null>(null);\n  const sideAt = (x: number) => {\n    const width = ref.current?.offsetWidth ?? 0;\n    const startHalf = x < width / 2;\n    return startHalf === (direction === \"ltr\") ? \"before\" : \"after\";\n  };\n\n  const { dragProps, isDragging } = useDrag({\n    getItems: () => [{ [COLUMN_DRAG_TYPE]: column.id, \"text/plain\": getColumnHeaderLabel(column) }],\n  });\n  const { dropProps } = useDrop({\n    ref,\n    getDropOperation: (types) => (types.has(COLUMN_DRAG_TYPE) && !isDragging ? \"move\" : \"cancel\"),\n    onDropEnter: (event) => setSide(sideAt(event.x)),\n    onDropMove: (event) => setSide(sideAt(event.x)),\n    onDropExit: () => setSide(null),\n    onDrop: async (event) => {\n      const at = sideAt(event.x);\n      setSide(null);\n      const item = event.items.find(\n        (item) => isTextDropItem(item) && item.types.has(COLUMN_DRAG_TYPE),\n      );\n      if (!item || !isTextDropItem(item)) return;\n      const id = await item.getText(COLUMN_DRAG_TYPE);\n      if (id === column.id) return;\n      const current = table.store.state.columnOrder.length\n        ? table.store.state.columnOrder\n        : table.getAllLeafColumns().map((leaf) => leaf.id);\n      const next = current.filter((key) => key !== id);\n      const index = next.indexOf(column.id);\n      if (index === -1) return;\n      next.splice(at === \"after\" ? index + 1 : index, 0, id);\n      table.setColumnOrder(next);\n    },\n  });\n\n  return (\n    <div\n      ref={ref}\n      {...dragProps}\n      {...dropProps}\n      data-slot=\"data-grid-column-drag-area\"\n      data-dragging={isDragging || undefined}\n      data-drop-side={side ?? undefined}\n      className={cn(\n        // Spans the cell's padding, so the drop line sits on the column edge.\n        \"relative min-w-0 flex-1 cursor-grab py-2.5 data-dragging:opacity-50\",\n        dense ? \"-mx-2.5 px-2.5\" : \"-mx-4 px-4\",\n        \"before:pointer-events-none before:absolute before:inset-y-0 before:w-0.5 before:bg-primary before:opacity-0 data-drop-side:before:opacity-100 data-[drop-side=after]:before:end-0 data-[drop-side=before]:before:start-0\",\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n\n/**\n * Indents a tree row's content by its depth: sets `--data-grid-tree-padding`\n * and uses it as the inline-start padding. For custom tree cells.\n */\nfunction getDataGridTreeIndentStyle<TData extends RowData>(\n  row: TanstackRow<DataGridFeatures, TData>,\n  indent = 20,\n): React.CSSProperties {\n  return {\n    \"--data-grid-tree-padding\": `${row.depth * indent}px`,\n    paddingInlineStart: \"var(--data-grid-tree-padding)\",\n  } as React.CSSProperties;\n}\n\n/**\n * Expands a row: shows its `getSubRows` children (tree rows) or the\n * full-width details of `renderExpandedRow` on `DataGridTable`. Put it in a\n * cell, before the label in tree rows: it indents by the row's depth and\n * leaves a spacer on leaves so labels line up. `children` replaces the chevron.\n */\nfunction DataGridRowExpand<TData extends RowData>({\n  row,\n  indent = 20,\n  className,\n  children,\n}: {\n  row: TanstackRow<DataGridFeatures, TData>;\n  /** Inline offset per tree depth, in pixels. */\n  indent?: number;\n  className?: string;\n  children?: React.ReactNode;\n}) {\n  const labels = useDataGridLabels();\n  const isExpanded = row.getIsExpanded();\n  return (\n    <span\n      data-slot=\"data-grid-table-row-expand\"\n      style={getDataGridTreeIndentStyle(row, indent)}\n      className={cn(\"inline-flex shrink-0 items-center\", className)}\n    >\n      {row.getCanExpand() ? (\n        <Button\n          variant=\"ghost\"\n          size=\"icon-sm\"\n          aria-label={isExpanded ? labels.collapseRow : labels.expandRow}\n          aria-expanded={isExpanded}\n          onPress={() => row.toggleExpanded()}\n        >\n          {children ??\n            (isExpanded ? (\n              <ChevronDownIcon aria-hidden=\"true\" />\n            ) : (\n              <ChevronRightIcon aria-hidden=\"true\" className=\"rtl:rotate-180\" />\n            ))}\n        </Button>\n      ) : (\n        <span aria-hidden=\"true\" className=\"size-4\" />\n      )}\n    </span>\n  );\n}\n\n/** Row selection checkbox: in a header it selects the page, in a cell its row. */\nfunction DataGridRowSelect(props: Omit<React.ComponentProps<typeof Checkbox>, \"slot\">) {\n  return <Checkbox slot=\"selection\" {...props} />;\n}\n\n/** Drag handle for `onRowsReorder`; put it in a cell. */\nfunction DataGridRowDragHandle({ className, ...props }: React.ComponentProps<typeof AriaButton>) {\n  return (\n    <AriaButton\n      slot=\"drag\"\n      className={cn(\n        \"flex size-7 cursor-grab items-center justify-center rounded-md text-muted-foreground outline-none data-focus-visible:ring-2 data-focus-visible:ring-ring/50 data-hovered:bg-muted\",\n        className as string,\n      )}\n      {...props}\n    >\n      <GripVerticalIcon aria-hidden=\"true\" className=\"size-4\" />\n    </AriaButton>\n  );\n}\n\n/**\n * A row's actions in a menu behind an icon button; put it in a cell. Children\n * are `DropdownMenuItem`s; pressing the button or an item doesn't trigger\n * `onRowClick`. With no items (all hidden), the menu says so instead of\n * opening empty. `rowLabel` names the button (\"Actions for …\").\n */\nfunction DataGridRowActions({\n  rowLabel,\n  \"aria-label\": ariaLabel,\n  children,\n  className,\n}: {\n  rowLabel?: string;\n  \"aria-label\"?: string;\n  children?: React.ReactNode;\n  className?: string;\n}) {\n  const labels = useDataGridLabels();\n  const hasActions = React.Children.toArray(children).length > 0;\n  return (\n    <DropdownMenuTrigger>\n      <Button\n        variant=\"ghost\"\n        size=\"icon-sm\"\n        aria-label={ariaLabel ?? labels.rowActions(rowLabel)}\n        className={className}\n      >\n        <EllipsisVerticalIcon aria-hidden=\"true\" />\n      </Button>\n      <DropdownMenu placement=\"bottom end\" className=\"w-auto min-w-44\">\n        {hasActions ? (\n          children\n        ) : (\n          <DropdownMenuItem id=\"no-actions\" isDisabled>\n            {labels.noRowActions}\n          </DropdownMenuItem>\n        )}\n      </DropdownMenu>\n    </DropdownMenuTrigger>\n  );\n}\n\n/**\n * What is selected and what to do with it. Shows while rows are selected:\n * the count (\"3 selected\", plus \"(1 on other pages)\" when part of the\n * selection isn't on screen), your actions as children and \"Clear selection\".\n * The count is announced from a status region that stays mounted, so the\n * first selection is read too. Put it above or below the `DataGridContainer`.\n */\nfunction DataGridSelectionBar({\n  children,\n  className,\n}: {\n  children?: React.ReactNode;\n  className?: string;\n}) {\n  const {\n    table,\n    i18n: { labels },\n  } = useDataGrid();\n  const barRef = React.useRef<HTMLDivElement>(null);\n  const { rowSelection } = table.store.state;\n  const selected = Object.keys(rowSelection).filter((id) => rowSelection[id]);\n  const pageIds = new Set(table.getRowModel().rows.map((row) => row.id));\n  const offPage = selected.filter((id) => !pageIds.has(id)).length;\n  const summary =\n    selected.length > 0\n      ? [labels.selectedRows(selected.length), offPage > 0 && labels.selectedOnOtherPages(offPage)]\n          .filter(Boolean)\n          .join(\" \")\n      : \"\";\n\n  const clear = () => {\n    // The bar unmounts with the selection; focus goes back to the grid.\n    barRef.current\n      ?.closest(\"[data-slot=data-grid]\")\n      ?.querySelector<HTMLElement>(\"[role=grid]\")\n      ?.focus();\n    table.resetRowSelection(true);\n  };\n\n  return (\n    <>\n      <span role=\"status\" data-slot=\"data-grid-selection-status\" className=\"sr-only\">\n        {summary}\n      </span>\n      {selected.length > 0 && (\n        <div\n          ref={barRef}\n          data-slot=\"data-grid-selection-bar\"\n          className={cn(\n            \"flex flex-wrap items-center gap-2 rounded-lg bg-muted px-3 py-1.5 text-sm\",\n            className,\n          )}\n        >\n          {/* Read from the status region; hidden here so it isn't read twice. */}\n          <span aria-hidden=\"true\" className=\"me-auto font-medium tabular-nums\">\n            {summary}\n          </span>\n          {children}\n          <Button variant=\"ghost\" size=\"sm\" onPress={clear}>\n            {labels.clearSelection}\n          </Button>\n        </div>\n      )}\n    </>\n  );\n}\n\n/** Pins a row to the top, or to the bottom (TanStack row pinning); put it in a cell. */\nfunction DataGridRowPin<TData extends RowData>({\n  row,\n  position = \"top\",\n}: {\n  row: TanstackRow<DataGridFeatures, TData>;\n  position?: \"top\" | \"bottom\";\n}) {\n  const labels = useDataGridLabels();\n  const isPinned = row.getIsPinned();\n  return (\n    <Button\n      variant=\"ghost\"\n      size=\"icon-sm\"\n      aria-label={isPinned ? labels.unpinRow : labels.pinRow}\n      aria-pressed={!!isPinned}\n      onPress={() => row.pin(isPinned ? false : position)}\n    >\n      {isPinned ? <PinOffIcon aria-hidden=\"true\" /> : <PinIcon aria-hidden=\"true\" />}\n    </Button>\n  );\n}\n\ntype DataGridColumnHeaderProps<TData extends RowData, TValue extends CellData> = {\n  column: DataGridColumn<TData, TValue>;\n  /** Defaults to `getColumnHeaderLabel(column)`. */\n  title?: string;\n  icon?: React.ReactNode;\n  /** Adds the column visibility submenu (with `tableLayout.columnsVisibility`). */\n  visibility?: boolean;\n  className?: string;\n};\n\n/**\n * A header with a sort indicator. With pinning, moving or visibility enabled\n * in `tableLayout`, the title opens a menu with those actions and sorting.\n */\nfunction DataGridColumnHeader<TData extends RowData, TValue extends CellData>({\n  column,\n  title,\n  icon,\n  visibility = false,\n  className,\n}: DataGridColumnHeaderProps<TData, TValue>) {\n  const {\n    table,\n    layout,\n    isLoading,\n    i18n: { labels },\n  } = useDataGrid<TData>();\n  const label = title ?? getColumnHeaderLabel(column);\n  const sorted = column.getIsSorted();\n  const pinned = column.getIsPinned();\n  const canSort = column.getCanSort();\n  const canPin = !!layout.columnsPinnable && column.getCanPin();\n  const order = table.store.state.columnOrder.length\n    ? table.store.state.columnOrder\n    : table.getAllLeafColumns().map((leaf) => leaf.id);\n  const index = order.indexOf(column.id);\n\n  const sortIcon = canSort ? (\n    sorted === \"desc\" ? (\n      <ArrowDownIcon aria-hidden=\"true\" className=\"size-3.5\" />\n    ) : sorted === \"asc\" ? (\n      <ArrowUpIcon aria-hidden=\"true\" className=\"size-3.5\" />\n    ) : (\n      <ChevronsUpDownIcon aria-hidden=\"true\" className=\"size-3.5 opacity-60\" />\n    )\n  ) : null;\n\n  const hasMenu = canPin || layout.columnsMovable || (layout.columnsVisibility && visibility);\n\n  if (!hasMenu) {\n    return (\n      <span className={cn(\"inline-flex items-center gap-1.5\", className)}>\n        {icon}\n        {label}\n        {sortIcon}\n      </span>\n    );\n  }\n\n  const move = (offset: number) => {\n    const next = [...order];\n    const [moved] = next.splice(index, 1);\n    if (moved === undefined) return;\n    next.splice(index + offset, 0, moved);\n    table.setColumnOrder(next);\n  };\n\n  const onAction = (key: Key) => {\n    switch (key) {\n      case \"asc\":\n      case \"desc\":\n        if (sorted === key) column.clearSorting();\n        else column.toggleSorting(key === \"desc\");\n        break;\n      case \"pin-start\":\n        column.pin(pinned === \"start\" ? false : \"start\");\n        break;\n      case \"pin-end\":\n        column.pin(pinned === \"end\" ? false : \"end\");\n        break;\n      case \"move-left\":\n        move(-1);\n        break;\n      case \"move-right\":\n        move(1);\n        break;\n      default:\n        table.getColumn(String(key))?.toggleVisibility();\n    }\n  };\n\n  const hideable = table.getAllLeafColumns().filter((leaf) => leaf.getCanHide());\n  const check = (isOn: boolean) =>\n    isOn ? <CheckIcon aria-hidden=\"true\" className=\"ml-auto text-primary\" /> : null;\n\n  return (\n    <div className=\"-ms-2 flex items-center gap-1\">\n      <DropdownMenuTrigger>\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          isDisabled={isLoading}\n          className={cn(\"h-7 gap-1.5 px-2 font-medium text-muted-foreground\", className)}\n        >\n          {icon}\n          {label}\n          {sortIcon}\n        </Button>\n        <DropdownMenu aria-label={labels.columnMenu(label)} className=\"w-44\" onAction={onAction}>\n          {canSort && (\n            <DropdownMenuGroup aria-label={labels.sort}>\n              <DropdownMenuItem id=\"asc\" textValue={labels.sortAscending}>\n                <ArrowUpIcon aria-hidden=\"true\" />\n                {labels.sortAscending}\n                {check(sorted === \"asc\")}\n              </DropdownMenuItem>\n              <DropdownMenuItem id=\"desc\" textValue={labels.sortDescending}>\n                <ArrowDownIcon aria-hidden=\"true\" />\n                {labels.sortDescending}\n                {check(sorted === \"desc\")}\n              </DropdownMenuItem>\n            </DropdownMenuGroup>\n          )}\n          {canPin && (\n            <>\n              {canSort && <DropdownMenuSeparator />}\n              <DropdownMenuGroup aria-label={labels.pin}>\n                <DropdownMenuItem id=\"pin-start\" textValue={labels.pinColumnStart}>\n                  <ArrowLeftToLineIcon aria-hidden=\"true\" className=\"rtl:rotate-180\" />\n                  {labels.pinColumnStart}\n                  {check(pinned === \"start\")}\n                </DropdownMenuItem>\n                <DropdownMenuItem id=\"pin-end\" textValue={labels.pinColumnEnd}>\n                  <ArrowRightToLineIcon aria-hidden=\"true\" className=\"rtl:rotate-180\" />\n                  {labels.pinColumnEnd}\n                  {check(pinned === \"end\")}\n                </DropdownMenuItem>\n              </DropdownMenuGroup>\n            </>\n          )}\n          {layout.columnsMovable && (\n            <>\n              <DropdownMenuSeparator />\n              <DropdownMenuGroup aria-label={labels.move}>\n                <DropdownMenuItem id=\"move-left\" isDisabled={index <= 0 || !!pinned}>\n                  <ArrowLeftIcon aria-hidden=\"true\" />\n                  {labels.moveColumnLeft}\n                </DropdownMenuItem>\n                <DropdownMenuItem\n                  id=\"move-right\"\n                  isDisabled={index >= order.length - 1 || !!pinned}\n                >\n                  <ArrowRightIcon aria-hidden=\"true\" />\n                  {labels.moveColumnRight}\n                </DropdownMenuItem>\n              </DropdownMenuGroup>\n            </>\n          )}\n          {layout.columnsVisibility && visibility && (\n            <>\n              <DropdownMenuSeparator />\n              <DropdownMenuGroup aria-label={labels.columns}>\n                <DropdownMenuLabel>{labels.columns}</DropdownMenuLabel>\n                {hideable.map((leaf) => (\n                  <DropdownMenuItem\n                    key={leaf.id}\n                    id={leaf.id}\n                    textValue={getColumnHeaderLabel(leaf)}\n                  >\n                    {getColumnHeaderLabel(leaf)}\n                    {check(leaf.getIsVisible())}\n                  </DropdownMenuItem>\n                ))}\n              </DropdownMenuGroup>\n            </>\n          )}\n        </DropdownMenu>\n      </DropdownMenuTrigger>\n      {canPin && pinned && (\n        <Button\n          variant=\"ghost\"\n          size=\"icon-xs\"\n          aria-label={labels.unpinColumn(label)}\n          onPress={() => column.pin(false)}\n        >\n          <PinOffIcon aria-hidden=\"true\" className=\"opacity-60\" />\n        </Button>\n      )}\n    </div>\n  );\n}\n\n/** A menu to show or hide columns; `trigger` is the button that opens it. */\nfunction DataGridColumnVisibility<TData extends RowData>({\n  table,\n  trigger,\n}: {\n  table: TanstackTable<DataGridFeatures, TData>;\n  trigger: React.ReactElement;\n}) {\n  const labels = useDataGridLabels();\n  const hideable = table.getAllLeafColumns().filter((column) => column.getCanHide());\n  return (\n    <DropdownMenuTrigger>\n      {trigger}\n      <DropdownMenu\n        aria-label={labels.columns}\n        placement=\"bottom end\"\n        className=\"min-w-40\"\n        selectionMode=\"multiple\"\n        selectedKeys={hideable.filter((column) => column.getIsVisible()).map((column) => column.id)}\n        onSelectionChange={(keys) => {\n          const visible = keys === \"all\" ? null : new Set([...keys].map(String));\n          for (const column of hideable)\n            column.toggleVisibility(!visible || visible.has(column.id));\n        }}\n      >\n        <DropdownMenuGroup>\n          <DropdownMenuLabel>{labels.toggleColumns}</DropdownMenuLabel>\n          {hideable.map((column) => (\n            <DropdownMenuItem\n              key={column.id}\n              id={column.id}\n              textValue={getColumnHeaderLabel(column)}\n            >\n              {getColumnHeaderLabel(column)}\n            </DropdownMenuItem>\n          ))}\n        </DropdownMenuGroup>\n      </DropdownMenu>\n    </DropdownMenuTrigger>\n  );\n}\n\ntype DataGridColumnFilterProps<TData extends RowData, TValue extends CellData> = {\n  column?: DataGridColumn<TData, TValue>;\n  title?: string;\n  options: { label: string; value: string; icon?: React.ComponentType<{ className?: string }> }[];\n};\n\n/**\n * A faceted filter: a button opening a searchable list of options. The\n * column's filter value becomes the array of selected values (pair it with\n * `filterFn: \"arrHas\"`); counts come from `getFacetedUniqueValues`.\n */\nfunction DataGridColumnFilter<TData extends RowData, TValue extends CellData>({\n  column,\n  title,\n  options,\n}: DataGridColumnFilterProps<TData, TValue>) {\n  const labels = useDataGridLabels();\n  const [query, setQuery] = React.useState(\"\");\n  const facets = column?.getFacetedUniqueValues?.();\n  const value = column?.getFilterValue();\n  const selected = new Set(Array.isArray(value) ? (value as string[]) : []);\n  const shown = options.filter((option) =>\n    option.label.toLowerCase().includes(query.toLowerCase()),\n  );\n  const setSelected = (values: string[]) =>\n    column?.setFilterValue(values.length ? values : undefined);\n\n  return (\n    <DialogTrigger>\n      <Button variant=\"outline\" size=\"sm\" className=\"border-dashed\">\n        <CirclePlusIcon aria-hidden=\"true\" />\n        {title}\n        {selected.size > 0 && (\n          <Badge variant=\"secondary\" className=\"ms-1 px-1 font-normal\">\n            {selected.size > 2\n              ? labels.filterSelected(selected.size)\n              : options\n                  .filter((option) => selected.has(option.value))\n                  .map((option) => option.label)\n                  .join(\", \")}\n          </Badge>\n        )}\n      </Button>\n      <Popover placement=\"bottom start\" className=\"w-56 gap-0 p-0\">\n        <Dialog aria-label={title ?? labels.filter} className=\"outline-none\">\n          <div className=\"p-2\">\n            <Input\n              aria-label={labels.filterSearch(title ?? labels.filter)}\n              placeholder={title}\n              value={query}\n              onChange={(event) => setQuery(event.target.value)}\n              className=\"h-8\"\n            />\n          </div>\n          <ListBox\n            aria-label={title ?? labels.filter}\n            selectionMode=\"multiple\"\n            // Escape closes the popover instead of clearing the filter.\n            escapeKeyBehavior=\"none\"\n            selectedKeys={selected}\n            onSelectionChange={(keys) =>\n              setSelected(\n                keys === \"all\" ? options.map((option) => option.value) : [...keys].map(String),\n              )\n            }\n            items={shown}\n            renderEmptyState={() => (\n              <div className=\"py-6 text-center text-sm text-muted-foreground\">\n                {labels.filterNoResults}\n              </div>\n            )}\n            className=\"max-h-72 overflow-y-auto p-1 outline-none\"\n          >\n            {(option) => (\n              <ListBoxItem\n                id={option.value}\n                textValue={option.label}\n                className=\"group/option flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none data-focused:bg-accent data-focused:text-accent-foreground\"\n              >\n                <span className=\"flex size-4 items-center justify-center rounded-sm border border-primary opacity-50 group-data-selected/option:bg-primary group-data-selected/option:text-primary-foreground group-data-selected/option:opacity-100\">\n                  <CheckIcon\n                    aria-hidden=\"true\"\n                    className=\"size-3.5 invisible group-data-selected/option:visible\"\n                  />\n                </span>\n                {option.icon && <option.icon className=\"size-4 text-muted-foreground\" />}\n                <span>{option.label}</span>\n                {facets?.get(option.value) !== undefined && (\n                  <span className=\"ms-auto font-mono text-xs\">{facets.get(option.value)}</span>\n                )}\n              </ListBoxItem>\n            )}\n          </ListBox>\n          {selected.size > 0 && (\n            <div className=\"border-t p-1\">\n              <Button variant=\"ghost\" size=\"sm\" className=\"w-full\" onPress={() => setSelected([])}>\n                {labels.filterClear}\n              </Button>\n            </div>\n          )}\n        </Dialog>\n      </Popover>\n    </DialogTrigger>\n  );\n}\n\ntype DataGridPaginationProps = {\n  sizes?: number[];\n  /** Shown instead of the page size selector while loading. */\n  sizesSkeleton?: React.ReactNode;\n  /** \"{from}\", \"{to}\" and \"{count}\" are replaced. Defaults to `i18n` `paginationInfo`. */\n  info?: string;\n  /** Shown instead of the record info while loading. */\n  infoSkeleton?: React.ReactNode;\n  rowsPerPageLabel?: string;\n  previousPageLabel?: string;\n  nextPageLabel?: string;\n  /**\n   * Page buttons when the row is truncated, counting the first and last page\n   * (rounded down to an odd number, at least 3). An ellipsis stands in for\n   * each hidden stretch.\n   */\n  moreLimit?: number;\n  ellipsisText?: string;\n  className?: string;\n};\n\n/**\n * The page row: every page when they fit, else the first page, a run around\n * the current one and the last page, with `null` for each hidden stretch. It\n * always takes `limit + 2` slots, so the row keeps its width while paging.\n */\nfunction getPageWindow(pageIndex: number, pageCount: number, moreLimit: number) {\n  const limit = Math.max(3, moreLimit % 2 === 0 ? moreLimit - 1 : moreLimit);\n  const slots = limit + 2;\n  const range = (start: number, end: number) =>\n    Array.from({ length: end - start + 1 }, (_, i) => start + i);\n  if (pageCount <= slots) return range(0, pageCount - 1);\n  const last = pageCount - 1;\n  const side = (limit - 3) / 2;\n  // Near an edge the run grows into the ellipsis slot that edge doesn't need.\n  if (pageIndex <= side + 2) return [...range(0, slots - 3), null, last];\n  if (pageIndex >= last - side - 2) return [0, null, ...range(last - (slots - 3), last)];\n  return [0, null, ...range(pageIndex - side, pageIndex + side), null, last];\n}\n\nfunction DataGridPagination({\n  sizes = [5, 10, 25, 50, 100],\n  sizesSkeleton = <Skeleton className=\"h-8 w-44\" />,\n  info,\n  infoSkeleton = <Skeleton className=\"h-8 w-60\" />,\n  rowsPerPageLabel: rowsPerPageProp,\n  previousPageLabel: previousPageProp,\n  nextPageLabel: nextPageProp,\n  moreLimit = 5,\n  ellipsisText: ellipsisProp,\n  className,\n}: DataGridPaginationProps) {\n  const {\n    table,\n    recordCount,\n    isLoading,\n    i18n: { labels },\n  } = useDataGrid();\n  const rowsPerPageLabel = rowsPerPageProp ?? labels.rowsPerPage;\n  const previousPageLabel = previousPageProp ?? labels.previousPage;\n  const nextPageLabel = nextPageProp ?? labels.nextPage;\n  const ellipsisText = ellipsisProp ?? labels.ellipsis;\n  const { pageIndex, pageSize } = table.store.state.pagination;\n  const pageCount = table.getPageCount();\n  const from = recordCount === 0 ? 0 : pageIndex * pageSize + 1;\n  const to = Math.min((pageIndex + 1) * pageSize, recordCount);\n  const pages = getPageWindow(pageIndex, pageCount, moreLimit);\n\n  if (isLoading) {\n    return (\n      <div data-slot=\"data-grid-pagination\" className={cn(\"flex justify-between\", className)}>\n        {sizesSkeleton}\n        {infoSkeleton}\n      </div>\n    );\n  }\n\n  return (\n    <nav\n      aria-label={labels.pagination}\n      data-slot=\"data-grid-pagination\"\n      className={cn(\n        \"flex flex-col flex-wrap items-center justify-between gap-2.5 sm:flex-row\",\n        className,\n      )}\n    >\n      <div className=\"flex items-center gap-2.5\">\n        <Select\n          aria-label={rowsPerPageLabel}\n          value={String(pageSize)}\n          onChange={(key) => table.setPageSize(Number(key))}\n          className=\"flex items-center gap-2.5\"\n        >\n          <span className=\"text-sm text-muted-foreground\">{rowsPerPageLabel}</span>\n          <SelectTrigger size=\"sm\" className=\"w-18\">\n            <SelectValue />\n          </SelectTrigger>\n          <SelectContent>\n            {sizes.map((size) => (\n              <SelectItem key={size} id={String(size)}>\n                {String(size)}\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n      </div>\n      <div className=\"flex flex-col items-center gap-2.5 sm:flex-row\">\n        <span className=\"text-sm text-nowrap text-muted-foreground\">\n          {info !== undefined\n            ? info\n                .replaceAll(\"{from}\", String(from))\n                .replaceAll(\"{to}\", String(to))\n                .replaceAll(\"{count}\", String(recordCount))\n            : labels.paginationInfo({ from, to, count: recordCount })}\n        </span>\n        {pageCount > 1 && (\n          <div className=\"flex items-center gap-1\">\n            <Button\n              variant=\"ghost\"\n              size=\"icon-sm\"\n              aria-label={previousPageLabel}\n              isDisabled={!table.getCanPreviousPage()}\n              onPress={() => table.previousPage()}\n            >\n              <ChevronLeftIcon aria-hidden=\"true\" className=\"rtl:rotate-180\" />\n            </Button>\n            {pages.map((page, index) => {\n              if (page === null) {\n                // Jumps to the middle of the hidden stretch.\n                const before = pages[index - 1] ?? 0;\n                const after = pages[index + 1] ?? pageCount - 1;\n                const target = Math.floor((before + after) / 2);\n                return (\n                  <Button\n                    key={index === 1 ? \"ellipsis-start\" : \"ellipsis-end\"}\n                    variant=\"ghost\"\n                    size=\"icon-sm\"\n                    aria-label={labels.goToPage(target + 1)}\n                    className=\"text-muted-foreground\"\n                    onPress={() => table.setPageIndex(target)}\n                  >\n                    {ellipsisText}\n                  </Button>\n                );\n              }\n              return (\n                <Button\n                  key={page}\n                  variant=\"ghost\"\n                  size=\"icon-sm\"\n                  aria-label={labels.goToPage(page + 1)}\n                  aria-current={page === pageIndex ? \"page\" : undefined}\n                  className=\"text-muted-foreground aria-[current=page]:bg-accent aria-[current=page]:text-accent-foreground\"\n                  onPress={() => table.setPageIndex(page)}\n                >\n                  {page + 1}\n                </Button>\n              );\n            })}\n            <Button\n              variant=\"ghost\"\n              size=\"icon-sm\"\n              aria-label={nextPageLabel}\n              isDisabled={!table.getCanNextPage()}\n              onPress={() => table.nextPage()}\n            >\n              <ChevronRightIcon aria-hidden=\"true\" className=\"rtl:rotate-180\" />\n            </Button>\n          </div>\n        )}\n      </div>\n    </nav>\n  );\n}\n\nexport type {\n  DataGridColumn,\n  DataGridColumnFilterProps,\n  DataGridColumnHeaderProps,\n  DataGridColumnMeta,\n  DataGridFeatures,\n  DataGridI18nConfig,\n  DataGridI18nLabels,\n  DataGridI18nOverrides,\n  DataGridLayout,\n  DataGridPaginationProps,\n  DataGridProps,\n  DataGridTableClassNames,\n  DataGridTableProps,\n};\nexport {\n  DataGrid,\n  DataGridColumnFilter,\n  DataGridColumnHeader,\n  DataGridColumnVisibility,\n  DataGridContainer,\n  DataGridPagination,\n  DataGridRowActions,\n  DataGridRowDragHandle,\n  DataGridRowExpand,\n  DataGridRowPin,\n  DataGridRowSelect,\n  DataGridSelectionBar,\n  DataGridTable,\n  DataGridTableFoot,\n  DataGridTableFootRow,\n  DataGridTableFootRowCell,\n  dataGridFeatures,\n  defaultDataGridI18n,\n  getColumnHeaderLabel,\n  getDataGridTreeIndentStyle,\n  mergeDataGridI18n,\n  useDataGrid,\n};\n",
      "type": "registry:component",
      "target": "components/fma-ds/data-grid.tsx"
    }
  ],
  "meta": {
    "status": "stable",
    "source": "third-party",
    "license": "MIT",
    "origin": "reui.io (https://reui.io/docs/components/base/data-grid, MIT); rendering rebuilt on react-aria Table",
    "tags": [
      "data-display",
      "table",
      "grid",
      "tanstack",
      "react-aria"
    ],
    "contentHash": "1fe5772f3aab"
  },
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}