{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-grid",
  "title": "Data Grid",
  "author": "@fma-ui/ui",
  "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. Create the table with `useTable({ features: dataGridFeatures, ... })`: the exported features cover sorting, filtering with facets, pagination, selection, row expanding, 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`. Column `meta` takes `headerTitle`, `skeleton`, `autoSize`, `headerClassName` and `cellClassName`. Wrap parts in `DataGrid table={table} recordCount={…}`: `DataGridTable`, `DataGridPagination`, `DataGridColumnHeader` (sort indicator, plus a menu for pinning, moving and visibility when `tableLayout` enables them), `DataGridColumnVisibility`, `DataGridColumnFilter` (faceted, use `filterFn: \"arrHas\"`), `DataGridRowSelect` (with `enableRowSelection`), `DataGridRowPin`, `DataGridRowDragHandle` (with `onRowsReorder`) and `DataGridRowExpand` (with `getRowCanExpand` and `DataGridTable`'s `renderExpandedRow`, a full-width details row). `tableLayout.columnsDraggable` lets users drag headers to reorder columns (keyboard users use the header menu's move actions). `isLoading` shows skeleton rows (`meta.skeleton`) or a spinner; `meta.autoSize` columns take the remaining width (not measured from content).",
  "dependencies": [
    "@tanstack/react-table@^9.2.5",
    "cn@^0.4.0",
    "lucide-react@^1.52.0",
    "react-aria-components@^1.21.1"
  ],
  "registryDependencies": [
    "@fma-ui/badge",
    "@fma-ui/button",
    "@fma-ui/checkbox",
    "@fma-ui/dropdown-menu",
    "@fma-ui/input",
    "@fma-ui/popover",
    "@fma-ui/select",
    "@fma-ui/skeleton",
    "@fma-ui/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  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  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  TableHeader,\n  TableLayout,\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\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` 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  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  /** Alternating row background. */\n  stripped?: boolean;\n  headerBackground?: 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  /** 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\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  emptyMessage?: React.ReactNode;\n  /** Makes rows actionable: pressing one (or Enter on it) calls this. */\n  onRowClick?: (row: TData) => void;\n  tableLayout?: DataGridLayout;\n  className?: string;\n  children?: React.ReactNode;\n};\n\ntype DataGridContextValue = Omit<DataGridProps<RowData>, \"table\" | \"children\"> & {\n  table: TanstackTable<DataGridFeatures, RowData>;\n  layout: DataGridLayout;\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/** 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  ...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\">),\n    table: table as TanstackTable<DataGridFeatures, RowData>,\n    layout: { rowBorder: true, width: \"fixed\", ...tableLayout },\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): React.CSSProperties | undefined {\n  const pinned = column.getIsPinned();\n  if (!pinned) return undefined;\n  return {\n    position: \"sticky\",\n    zIndex: 1,\n    ...(pinned === \"start\"\n      ? { insetInlineStart: column.getStart(\"start\") }\n      : { insetInlineEnd: column.getAfter(\"end\") }),\n  };\n}\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\n// Key suffix of the full-width row rendered under an expanded row.\nconst EXPANDED = \":expanded\";\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\": ariaLabel = \"Data grid\",\n  className,\n  onRowsReorder,\n  isVirtualized,\n  rowHeight = 40,\n  renderExpandedRow,\n}: DataGridTableProps<TData>) {\n  const {\n    table,\n    layout,\n    isLoading,\n    loadingMode = \"skeleton\",\n    emptyMessage,\n    onRowClick,\n  } = useDataGrid();\n  const state = table.store.state;\n  const columns = [\n    ...table.getStartVisibleLeafColumns(),\n    ...table.getCenterVisibleLeafColumns(),\n    ...table.getEndVisibleLeafColumns(),\n  ];\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 rows = [...table.getTopRows(), ...table.getCenterRows()];\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  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      ]}\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      )}\n    >\n      <TableHeader className={cn(layout.headerSticky && \"sticky top-0\", overlapsRows && \"z-10\")}>\n        {columns.map((column) => {\n          const header = headers.get(column.id);\n          const width = column.columnDef.meta?.autoSize ? \"1fr\" : column.getSize();\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              style={pinningStyle(column)}\n              className={cn(\n                \"group/column relative h-10 border-b 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.cellBorder && \"border-e last:border-e-0\",\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(--background))]\"\n                  : (overlapsRows || column.getIsPinned()) && \"bg-background\",\n                column.columnDef.meta?.headerClassName,\n              )}\n            >\n              <div className=\"flex items-center gap-1\">\n                {layout.columnsDraggable && !showSkeleton ? (\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() && (\n                  <ColumnResizer\n                    aria-label={`Resize ${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        renderEmptyState={() => (\n          <div className=\"py-10 text-center text-sm text-muted-foreground\">\n            {emptyMessage ?? \"No data available.\"}\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-pinned={row.getIsPinned() || undefined}\n                className={cn(\n                  rowClassName(layout, isVirtualized),\n                  onRowClick && \"cursor-pointer\",\n                  \"data-pinned:bg-muted/40\",\n                )}\n              >\n                {[\n                  ...row.getStartVisibleCells(),\n                  ...row.getCenterVisibleCells(),\n                  ...row.getEndVisibleCells(),\n                ].map((cell) => (\n                  <Cell\n                    key={cell.id}\n                    style={pinningStyle(cell.column)}\n                    className={cn(\n                      cellPadding,\n                      cellClassName(layout, isVirtualized),\n                      cell.column.getIsPinned() && \"bg-background\",\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={`${String((rowHeaderId && row.getValue(rowHeaderId)) ?? row.id)} details`}\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      </TableBody>\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      data-slot=\"data-grid-table-container\"\n      aria-busy={isLoading || undefined}\n      className=\"relative\"\n    >\n      <ResizableTableContainer\n        onResize={(widths) => {\n          const sizing: Record<string, number> = {};\n          for (const [id, width] of widths)\n            if (typeof width === \"number\") sizing[String(id)] = width;\n          table.setColumnSizing((old) => ({ ...old, ...sizing }));\n        }}\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          className=\"absolute inset-0 z-20 flex items-center justify-center bg-background/60\"\n        >\n          <Spinner className=\"size-6\" aria-label=\"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    // 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  );\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 * Toggles a row's full-width details (`renderExpandedRow` on `DataGridTable`);\n * put it in a cell. Rows expand only when `getRowCanExpand` allows it.\n */\nfunction DataGridRowExpand<TData extends RowData>({\n  row,\n  className,\n}: {\n  row: TanstackRow<DataGridFeatures, TData>;\n  className?: string;\n}) {\n  if (!row.getCanExpand()) return null;\n  const isExpanded = row.getIsExpanded();\n  return (\n    <Button\n      variant=\"ghost\"\n      size=\"icon-sm\"\n      aria-label={isExpanded ? \"Collapse row\" : \"Expand row\"}\n      aria-expanded={isExpanded}\n      onPress={() => row.toggleExpanded()}\n      className={className}\n    >\n      {isExpanded ? (\n        <ChevronDownIcon aria-hidden=\"true\" />\n      ) : (\n        <ChevronRightIcon aria-hidden=\"true\" className=\"rtl:rotate-180\" />\n      )}\n    </Button>\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/** Pins a row to the top (TanStack row pinning); put it in a cell. */\nfunction DataGridRowPin<TData extends RowData>({\n  row,\n}: {\n  row: TanstackRow<DataGridFeatures, TData>;\n}) {\n  const isPinned = row.getIsPinned();\n  return (\n    <Button\n      variant=\"ghost\"\n      size=\"icon-sm\"\n      aria-label={isPinned ? \"Unpin row\" : \"Pin row\"}\n      aria-pressed={!!isPinned}\n      onPress={() => row.pin(isPinned ? false : \"top\")}\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 { table, layout, isLoading } = 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={`${label} column`} className=\"w-44\" onAction={onAction}>\n          {canSort && (\n            <DropdownMenuGroup aria-label=\"Sort\">\n              <DropdownMenuItem id=\"asc\" textValue=\"Ascending\">\n                <ArrowUpIcon aria-hidden=\"true\" />\n                Ascending\n                {check(sorted === \"asc\")}\n              </DropdownMenuItem>\n              <DropdownMenuItem id=\"desc\" textValue=\"Descending\">\n                <ArrowDownIcon aria-hidden=\"true\" />\n                Descending\n                {check(sorted === \"desc\")}\n              </DropdownMenuItem>\n            </DropdownMenuGroup>\n          )}\n          {canPin && (\n            <>\n              {canSort && <DropdownMenuSeparator />}\n              <DropdownMenuGroup aria-label=\"Pin\">\n                <DropdownMenuItem id=\"pin-start\" textValue=\"Pin to start\">\n                  <ArrowLeftToLineIcon aria-hidden=\"true\" className=\"rtl:rotate-180\" />\n                  Pin to start\n                  {check(pinned === \"start\")}\n                </DropdownMenuItem>\n                <DropdownMenuItem id=\"pin-end\" textValue=\"Pin to end\">\n                  <ArrowRightToLineIcon aria-hidden=\"true\" className=\"rtl:rotate-180\" />\n                  Pin to end\n                  {check(pinned === \"end\")}\n                </DropdownMenuItem>\n              </DropdownMenuGroup>\n            </>\n          )}\n          {layout.columnsMovable && (\n            <>\n              <DropdownMenuSeparator />\n              <DropdownMenuGroup aria-label=\"Move\">\n                <DropdownMenuItem id=\"move-left\" isDisabled={index <= 0 || !!pinned}>\n                  <ArrowLeftIcon aria-hidden=\"true\" />\n                  Move left\n                </DropdownMenuItem>\n                <DropdownMenuItem\n                  id=\"move-right\"\n                  isDisabled={index >= order.length - 1 || !!pinned}\n                >\n                  <ArrowRightIcon aria-hidden=\"true\" />\n                  Move right\n                </DropdownMenuItem>\n              </DropdownMenuGroup>\n            </>\n          )}\n          {layout.columnsVisibility && visibility && (\n            <>\n              <DropdownMenuSeparator />\n              <DropdownMenuGroup aria-label=\"Columns\">\n                <DropdownMenuLabel>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={`Unpin ${label} column`}\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 hideable = table.getAllLeafColumns().filter((column) => column.getCanHide());\n  return (\n    <DropdownMenuTrigger>\n      {trigger}\n      <DropdownMenu\n        aria-label=\"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>Toggle columns</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 [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              ? `${selected.size} selected`\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 ?? \"Filter\"} className=\"outline-none\">\n          <div className=\"p-2\">\n            <Input\n              aria-label={`Search ${title ?? \"options\"}`}\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 ?? \"Options\"}\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                No results found.\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                Clear filters\n              </Button>\n            </div>\n          )}\n        </Dialog>\n      </Popover>\n    </DialogTrigger>\n  );\n}\n\ntype DataGridPaginationProps = {\n  sizes?: number[];\n  /** \"{from}\", \"{to}\" and \"{count}\" are replaced. */\n  info?: string;\n  rowsPerPageLabel?: string;\n  previousPageLabel?: string;\n  nextPageLabel?: string;\n  /** How many page buttons show at once before an ellipsis. */\n  moreLimit?: number;\n  className?: string;\n};\n\nfunction DataGridPagination({\n  sizes = [5, 10, 25, 50, 100],\n  info = \"{from} - {to} of {count}\",\n  rowsPerPageLabel = \"Rows per page\",\n  previousPageLabel = \"Previous page\",\n  nextPageLabel = \"Next page\",\n  moreLimit = 5,\n  className,\n}: DataGridPaginationProps) {\n  const { table, recordCount, isLoading } = useDataGrid();\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 groupStart = Math.floor(pageIndex / moreLimit) * moreLimit;\n  const groupEnd = Math.min(groupStart + moreLimit, pageCount);\n  const pages = Array.from({ length: groupEnd - groupStart }, (_, i) => groupStart + i);\n\n  if (isLoading) {\n    return (\n      <div data-slot=\"data-grid-pagination\" className={cn(\"flex justify-between\", className)}>\n        <Skeleton className=\"h-8 w-44\" />\n        <Skeleton className=\"h-8 w-60\" />\n      </div>\n    );\n  }\n\n  return (\n    <nav\n      aria-label=\"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\n            .replaceAll(\"{from}\", String(from))\n            .replaceAll(\"{to}\", String(to))\n            .replaceAll(\"{count}\", String(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            {groupStart > 0 && (\n              <Button\n                variant=\"ghost\"\n                size=\"icon-sm\"\n                aria-label={`Page ${groupStart}`}\n                onPress={() => table.setPageIndex(groupStart - 1)}\n              >\n                …\n              </Button>\n            )}\n            {pages.map((page) => (\n              <Button\n                key={page}\n                variant=\"ghost\"\n                size=\"icon-sm\"\n                aria-label={`Page ${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            {groupEnd < pageCount && (\n              <Button\n                variant=\"ghost\"\n                size=\"icon-sm\"\n                aria-label={`Page ${groupEnd + 1}`}\n                onPress={() => table.setPageIndex(groupEnd)}\n              >\n                …\n              </Button>\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  DataGridLayout,\n  DataGridPaginationProps,\n  DataGridProps,\n  DataGridTableProps,\n};\nexport {\n  DataGrid,\n  DataGridColumnFilter,\n  DataGridColumnHeader,\n  DataGridColumnVisibility,\n  DataGridContainer,\n  DataGridPagination,\n  DataGridRowDragHandle,\n  DataGridRowExpand,\n  DataGridRowPin,\n  DataGridRowSelect,\n  DataGridTable,\n  dataGridFeatures,\n  getColumnHeaderLabel,\n  useDataGrid,\n};\n",
      "type": "registry:component",
      "target": "components/fma-ui/data-grid.tsx"
    }
  ],
  "meta": {
    "status": "stable",
    "source": "third-party",
    "origin": "reui.io (https://reui.io/docs/components/base/data-grid, MIT), via EMITTE; rendering rebuilt on react-aria Table",
    "tags": [
      "data-display",
      "table",
      "grid",
      "tanstack",
      "react-aria"
    ]
  },
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}