{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-grid",
  "title": "Data Grid",
  "description": "Data Grid from the Get IT Done design system.",
  "dependencies": [],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/checkbox.json",
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/pagination.json",
    "https://zero-design.itdone.de/r/skeleton.json",
    "https://zero-design.itdone.de/r/table.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/data-grid.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\nimport { cn } from '@/lib/utils';\nimport { Checkbox } from '@/components/ui/checkbox';\nimport { Pagination, type PaginationProps } from '@/components/ui/pagination';\nimport { useLabels } from '@/components/ui/labels-context';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport {\n  isTableCellElement,\n  type SortDirection,\n  Table,\n  TableBody,\n  TableCaption,\n  TableCell,\n  TableEmpty,\n  TableError,\n  TableFooter,\n  TableHead,\n  TableHeader,\n  TableLoadingMore,\n  TableRow,\n  type TableProps,\n} from '@/components/ui/table';\n\n// Data-driven grid built on the Table primitives: columns definition, row selection, sorting, density, state rows and a pager footer.\ntype DataGridDensity = 'default' | 'compact';\ntype DataGridAlign = 'left' | 'center' | 'right';\n// Row height: `default` is the single-line row, `two-line` fits a title with a subtitle (Table row-two-line size).\ntype DataGridRowHeight = 'default' | 'two-line';\n// Fixed column widths from the Figma Table first column variants (160, 200, 240, 360, 440). Unset columns share the remaining width.\ntype DataGridColumnWidth = 160 | 200 | 240 | 360 | 440;\n\n// Bound to the `size-column-<n>` sizing tokens. The fallback keeps the Figma value until the token ships in the installed tokens version.\nconst columnWidthClass: Record<DataGridColumnWidth, string> = {\n  160: 'w-[var(--size-column-160,160px)] min-w-[var(--size-column-160,160px)] max-w-[var(--size-column-160,160px)]',\n  200: 'w-[var(--size-column-200,200px)] min-w-[var(--size-column-200,200px)] max-w-[var(--size-column-200,200px)]',\n  240: 'w-[var(--size-column-240,240px)] min-w-[var(--size-column-240,240px)] max-w-[var(--size-column-240,240px)]',\n  360: 'w-[var(--size-column-360,360px)] min-w-[var(--size-column-360,360px)] max-w-[var(--size-column-360,360px)]',\n  440: 'w-[var(--size-column-440,440px)] min-w-[var(--size-column-440,440px)] max-w-[var(--size-column-440,440px)]',\n};\n\ninterface DataGridColumn<T> {\n  id: string;\n  header: React.ReactNode;\n  // Cell renderer. Without it the cell shows `String(row[id])`.\n  cell?: (row: T, index: number) => React.ReactNode;\n  align?: DataGridAlign;\n  // Shows the sort button in the header. Sorting is client side when `sortValue` is set, otherwise the consumer sorts via `onSortChange`.\n  sortable?: boolean;\n  sortValue?: (row: T) => string | number | Date | null | undefined;\n  // Hides the column on narrow viewports: `2` below md, `3` below lg. `1` (default) always shows.\n  priority?: 1 | 2 | 3;\n  className?: string;\n  // Pins the column to the start edge while the grid scrolls sideways (opaque in every row state, edge shadow while scrolled). Use it on the first column; with `selectable` the checkbox column pins too.\n  pinned?: boolean;\n  // Fixed width in px from the Figma column presets: 160, 200, 240, 360 or 440. The cell content truncates inside it.\n  width?: DataGridColumnWidth;\n  // How `cell` output is placed. `auto` (default) uses a returned TableCell-based component (TableLeadCell, TableBadgeCell, TableActionsCell, ...)\n  // as the cell itself and wraps anything else in a TableCell. `element` always uses the output as the cell, `cell` always wraps it.\n  cellAs?: 'auto' | 'cell' | 'element';\n}\n\ninterface DataGridSort {\n  columnId: string;\n  direction: Exclude<SortDirection, false>;\n}\n\ninterface DataGridPagination\n  extends Pick<\n    PaginationProps,\n    'page' | 'defaultPage' | 'onPageChange' | 'pageSize' | 'pageSizeOptions' | 'onPageSizeChange' | 'total' | 'rangeText' | 'perPageText' | 'loadingText'\n  > {}\n\ninterface DataGridProps<T> extends Omit<TableProps, 'density' | 'children'> {\n  columns: DataGridColumn<T>[];\n  rows: T[];\n  getRowId: (row: T) => string;\n  // Accessible name of the table (rendered as a screen reader caption).\n  label?: string;\n  density?: DataGridDensity;\n  // `two-line` gives every row the two-line height for cells that stack a title and a subtitle (see DataGridTwoLine).\n  rowHeight?: DataGridRowHeight;\n  // Row selection with a leading checkbox column. Controlled via `selectedIds`, uncontrolled via `defaultSelectedIds`.\n  selectable?: boolean;\n  selectedIds?: string[];\n  defaultSelectedIds?: string[];\n  onSelectedIdsChange?: (ids: string[]) => void;\n  // Sorting. Controlled via `sort`, uncontrolled via `defaultSort`.\n  sort?: DataGridSort | null;\n  defaultSort?: DataGridSort | null;\n  onSortChange?: (sort: DataGridSort | null) => void;\n  // Row whose record is open in a side sheet. Gets the active wash and `aria-current`, independent of selection (`aria-selected`).\n  activeRowId?: string | null;\n  // Makes rows actionable (hover, tab stop, Enter and Space).\n  onRowClick?: (row: T) => void;\n  getRowLabel?: (row: T) => string;\n  // State rows from Table: loading skeleton, error with retry, empty.\n  loading?: boolean;\n  error?: boolean;\n  onRetry?: () => void;\n  emptyTitle?: React.ReactNode;\n  emptyDescription?: React.ReactNode;\n  emptyActions?: React.ReactNode;\n  // Pager footer (compact Pagination). Slices `rows` client side unless `total` is given, then `rows` is the current page.\n  pagination?: DataGridPagination;\n  loadingRows?: number;\n  // Lazy loading footer row (use instead of `pagination`). `hasMore` offers the Load more button via `onLoadMore`, `loadingMore` shows the live loading status.\n  onLoadMore?: () => void;\n  hasMore?: boolean;\n  loadingMore?: boolean;\n  loadMoreLabel?: string;\n  loadingMoreLabel?: string;\n  selectAllLabel?: string;\n  selectRowLabel?: string | ((row: unknown) => string);\n}\n\nconst priorityClass = { 1: '', 2: 'max-md:hidden', 3: 'max-lg:hidden' } as const;\n\nconst toComparable = (value: string | number | Date | null | undefined) => (value instanceof Date ? value.getTime() : value);\n\nconst compareValues = (a: ReturnType<typeof toComparable>, b: ReturnType<typeof toComparable>) => {\n  // Empty values sort last in both directions, handled by the caller.\n  if (typeof a === 'number' && typeof b === 'number') return a - b;\n  return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' });\n};\n\nfunction DataGridInner<T>(\n  {\n    columns,\n    rows,\n    getRowId,\n    label,\n    density = 'default',\n    rowHeight,\n    selectable = false,\n    selectedIds: selectedIdsProp,\n    defaultSelectedIds = [],\n    onSelectedIdsChange,\n    sort: sortProp,\n    defaultSort = null,\n    onSortChange,\n    activeRowId,\n    onRowClick,\n    getRowLabel,\n    loading = false,\n    error = false,\n    onRetry,\n    emptyTitle,\n    emptyDescription,\n    emptyActions,\n    pagination,\n    loadingRows = 5,\n    onLoadMore,\n    hasMore = false,\n    loadingMore = false,\n    loadMoreLabel,\n    loadingMoreLabel,\n    selectAllLabel,\n    selectRowLabel,\n    className,\n    ...tableProps\n  }: DataGridProps<T>,\n  ref: React.ForwardedRef<HTMLTableElement>,\n) {\n  const l = useLabels({ selectAllRows: selectAllLabel, selectRow: typeof selectRowLabel === 'string' ? selectRowLabel : undefined });\n\n  const [innerSelected, setInnerSelected] = React.useState<string[]>(defaultSelectedIds);\n  const selectedControlled = selectedIdsProp !== undefined;\n  const selected = selectedControlled ? selectedIdsProp : innerSelected;\n  const setSelected = (next: string[]) => {\n    if (!selectedControlled) setInnerSelected(next);\n    onSelectedIdsChange?.(next);\n  };\n\n  const [innerSort, setInnerSort] = React.useState<DataGridSort | null>(defaultSort);\n  const sort = sortProp !== undefined ? sortProp : innerSort;\n  const changeSort = (columnId: string) => {\n    // Cycle ascending, descending, none.\n    const next: DataGridSort | null =\n      sort?.columnId !== columnId ? { columnId, direction: 'asc' } : sort.direction === 'asc' ? { columnId, direction: 'desc' } : null;\n    if (sortProp === undefined) setInnerSort(next);\n    onSortChange?.(next);\n  };\n\n  const sorted = React.useMemo(() => {\n    const column = sort ? columns.find((c) => c.id === sort.columnId) : undefined;\n    if (!sort || !column?.sortValue) return rows;\n    const read = column.sortValue;\n    const factor = sort.direction === 'asc' ? 1 : -1;\n    return [...rows].sort((a, b) => {\n      const va = toComparable(read(a));\n      const vb = toComparable(read(b));\n      const emptyA = va === null || va === undefined || va === '';\n      const emptyB = vb === null || vb === undefined || vb === '';\n      if (emptyA || emptyB) return emptyA === emptyB ? 0 : emptyA ? 1 : -1;\n      return factor * compareValues(va, vb);\n    });\n  }, [rows, columns, sort]);\n\n  const [innerPage, setInnerPage] = React.useState(pagination?.defaultPage ?? 1);\n  const [innerPageSize, setInnerPageSize] = React.useState(pagination?.pageSize ?? pagination?.pageSizeOptions?.[0] ?? 20);\n  const pageSize = pagination?.pageSize ?? innerPageSize;\n  const serverSide = pagination?.total !== undefined;\n  const total = pagination ? pagination.total ?? sorted.length : 0;\n  const pageCount = Math.max(1, Math.ceil(total / pageSize));\n  const page = Math.min(Math.max(1, pagination?.page ?? innerPage), pageCount);\n  const visible = pagination && !serverSide ? sorted.slice((page - 1) * pageSize, page * pageSize) : sorted;\n\n  const ids = visible.map(getRowId);\n  const selectedOnPage = ids.filter((id) => selected.includes(id));\n  const allChecked: boolean | 'indeterminate' = ids.length > 0 && selectedOnPage.length === ids.length ? true : selectedOnPage.length > 0 ? 'indeterminate' : false;\n  const toggleAll = (checked: boolean | 'indeterminate') => {\n    setSelected(checked === true ? Array.from(new Set([...selected, ...ids])) : selected.filter((id) => !ids.includes(id)));\n  };\n  const toggleRow = (id: string, checked: boolean | 'indeterminate') => {\n    setSelected(checked === true ? [...selected, id] : selected.filter((x) => x !== id));\n  };\n\n  const columnCount = columns.length + (selectable ? 1 : 0);\n  const compact = density === 'compact';\n  const pinnedSelect = selectable && columns.some((c) => c.pinned);\n  // With a pinned checkbox column the pinned data column sticks right after it.\n  const pinnedClass = (column: DataGridColumn<T>) => (column.pinned && selectable ? 'left-[var(--size-table-select)]' : undefined);\n  const state = loading ? 'loading' : error ? 'error' : visible.length === 0 ? 'empty' : 'rows';\n  const rowName = (row: T) => (typeof selectRowLabel === 'function' ? selectRowLabel(row) : getRowLabel ? `${l.selectRow}: ${getRowLabel(row)}` : l.selectRow);\n\n  return (\n    <Table\n      ref={ref}\n      density={compact ? 'zero-compact' : 'zero'}\n      headerTone=\"muted\"\n      rowHover=\"table\"\n      aria-busy={loading || undefined}\n      // Compact drops the type one step: head 13 to 12, cell 16 to 14.\n      className={cn(compact && '[&_th]:!typography-text-extra-small [&_td]:!typography-text-small', className)}\n      {...tableProps}\n    >\n      {label ? <TableCaption className=\"sr-only\">{label}</TableCaption> : null}\n      <TableHeader>\n        <TableRow>\n          {selectable ? (\n            <TableHead select pinned={pinnedSelect}>\n              <Checkbox size=\"sm\" aria-label={l.selectAllRows} checked={allChecked} disabled={state !== 'rows'} onCheckedChange={toggleAll} />\n            </TableHead>\n          ) : null}\n          {columns.map((column) => (\n            <TableHead\n              key={column.id}\n              align={column.align}\n              pinned={column.pinned}\n              sortable={column.sortable}\n              sortDirection={sort?.columnId === column.id ? sort.direction : false}\n              onSort={column.sortable ? () => changeSort(column.id) : undefined}\n              className={cn(priorityClass[column.priority ?? 1], column.width && columnWidthClass[column.width], pinnedClass(column), column.className)}\n            >\n              {column.header}\n            </TableHead>\n          ))}\n        </TableRow>\n      </TableHeader>\n      <TableBody>\n        {state === 'loading' ? (\n          // One skeleton cell per column with the same width, priority and pin classes as the real cells, so the columns stay under their headers.\n          Array.from({ length: loadingRows }).map((_, rowIndex) => (\n            <TableRow key={rowIndex}>\n              {selectable ? (\n                <TableCell select pinned={pinnedSelect}>\n                  <Skeleton aria-hidden=\"true\" className=\"h-4 w-4\" />\n                </TableCell>\n              ) : null}\n              {columns.map((column, colIndex) => (\n                <TableCell\n                  key={column.id}\n                  pinned={column.pinned}\n                  rowHeight={rowHeight}\n                  align={column.align === 'right' ? 'right' : undefined}\n                  className={cn(column.align === 'center' && 'text-center', priorityClass[column.priority ?? 1], column.width && columnWidthClass[column.width], pinnedClass(column), column.className)}\n                >\n                  <Skeleton aria-hidden=\"true\" className={cn('h-3 max-w-full', column.align === 'right' && 'ml-auto', column.align === 'center' && 'mx-auto', colIndex === 0 ? 'w-3/4' : 'w-1/2')} />\n                  {rowIndex === 0 && colIndex === 0 ? <span role=\"status\" className=\"sr-only\">{l.loadingTableData}</span> : null}\n                </TableCell>\n              ))}\n            </TableRow>\n          ))\n        ) : null}\n        {state === 'error' ? <TableError columns={columnCount} onRetry={onRetry} /> : null}\n        {state === 'empty' ? <TableEmpty columns={columnCount} title={emptyTitle} description={emptyDescription} actions={emptyActions} /> : null}\n        {state === 'rows'\n          ? visible.map((row, index) => {\n              const id = getRowId(row);\n              const isSelected = selected.includes(id);\n              return (\n                <TableRow\n                  key={id}\n                  selected={isSelected}\n                  active={activeRowId !== undefined && activeRowId !== null && id === activeRowId}\n                  onClick={onRowClick ? () => onRowClick(row) : undefined}\n                  aria-label={onRowClick && getRowLabel ? getRowLabel(row) : undefined}\n                >\n                  {selectable ? (\n                    // Keeps a checkbox click from activating the row.\n                    <TableCell select pinned={pinnedSelect} onClick={(event) => event.stopPropagation()}>\n                      <Checkbox size=\"sm\" aria-label={rowName(row)} checked={isSelected} onCheckedChange={(checked) => toggleRow(id, checked)} />\n                    </TableCell>\n                  ) : null}\n                  {columns.map((column) => {\n                    const content = column.cell ? column.cell(row, index) : String((row as Record<string, unknown>)[column.id] ?? '');\n                    const asElement = column.cellAs === 'element' || (column.cellAs !== 'cell' && isTableCellElement(content));\n                    const columnClass = cn(column.align === 'center' && 'text-center', priorityClass[column.priority ?? 1], column.width && columnWidthClass[column.width], pinnedClass(column), column.className);\n                    if (asElement && React.isValidElement<{ className?: string; align?: 'left' | 'right'; rowHeight?: DataGridRowHeight; pinned?: boolean }>(content)) {\n                      // A cell component returned by `cell` is the td itself, so the row keeps valid table markup.\n                      return React.cloneElement(content, {\n                        key: column.id,\n                        className: cn(columnClass, content.props.className),\n                        align: content.props.align ?? (column.align === 'right' ? 'right' : undefined),\n                        pinned: content.props.pinned ?? column.pinned,\n                        rowHeight: content.props.rowHeight ?? rowHeight,\n                      });\n                    }\n                    return (\n                      <TableCell key={column.id} pinned={column.pinned} rowHeight={rowHeight} align={column.align === 'right' ? 'right' : undefined} className={columnClass}>\n                        {content}\n                      </TableCell>\n                    );\n                  })}\n                </TableRow>\n              );\n            })\n          : null}\n        {state === 'rows' && !pagination && (loadingMore || (hasMore && onLoadMore)) ? (\n          <TableLoadingMore columns={columnCount} loading={loadingMore} onLoadMore={onLoadMore} label={loadMoreLabel} loadingLabel={loadingMoreLabel} />\n        ) : null}\n      </TableBody>\n      {pagination ? (\n        <TableFooter className=\"bg-card\">\n          <TableRow className=\"border-0\">\n            <TableCell colSpan={columnCount} className=\"h-auto p-0\">\n              <Pagination\n                variant=\"compact\"\n                page={page}\n                total={total}\n                pageSize={pageSize}\n                pageSizeOptions={pagination.pageSizeOptions}\n                rangeText={pagination.rangeText}\n                perPageText={pagination.perPageText}\n                loading={loading || loadingMore}\n                announce={!loading}\n                loadingText={pagination.loadingText}\n                onPageChange={(next) => {\n                  if (pagination.page === undefined) setInnerPage(next);\n                  pagination.onPageChange?.(next);\n                }}\n                onPageSizeChange={\n                  pagination.pageSizeOptions || pagination.onPageSizeChange\n                    ? (size) => {\n                        if (pagination.pageSize === undefined) setInnerPageSize(size);\n                        setInnerPage(1);\n                        pagination.onPageSizeChange?.(size);\n                      }\n                    : undefined\n                }\n              />\n            </TableCell>\n          </TableRow>\n        </TableFooter>\n      ) : null}\n    </Table>\n  );\n}\n\n// Generic forwardRef keeps the row type inferred from `rows` and `columns`.\nconst DataGrid = React.forwardRef(DataGridInner) as <T>(props: DataGridProps<T> & { ref?: React.ForwardedRef<HTMLTableElement> }) => ReturnType<typeof DataGridInner>;\n(DataGrid as unknown as { displayName: string }).displayName = 'DataGrid';\n\ninterface DataGridTwoLineProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {\n  title: React.ReactNode;\n  subtitle?: React.ReactNode;\n}\n\n// Title with a subtitle for a DataGrid `cell` in a `rowHeight=\"two-line\"` grid. Both lines truncate.\nconst DataGridTwoLine = React.forwardRef<HTMLDivElement, DataGridTwoLineProps>(({ title, subtitle, className, ...props }, ref) => (\n  <div ref={ref} className={cn('flex min-w-0 flex-col', className)} {...props}>\n    <span className=\"typography-ui-medium truncate text-text-default\">{title}</span>\n    {subtitle ? <span className=\"typography-text-extra-small truncate text-text-subtle\">{subtitle}</span> : null}\n  </div>\n));\nDataGridTwoLine.displayName = 'DataGridTwoLine';\n\nexport { DataGrid, DataGridTwoLine, type DataGridTwoLineProps, type DataGridRowHeight, type DataGridAlign, type DataGridColumn, type DataGridColumnWidth, type DataGridDensity, type DataGridPagination, type DataGridProps, type DataGridSort };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}