{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table",
  "title": "Table",
  "description": "Table from the Get IT Done design system.",
  "dependencies": [
    "class-variance-authority@^0.7.0",
    "lucide-react@^0.379.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/button.json",
    "https://zero-design.itdone.de/r/edge.json",
    "https://zero-design.itdone.de/r/empty-state.json",
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/loading-spinner.json",
    "https://zero-design.itdone.de/r/skeleton.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/table.tsx",
      "content": "'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { ChevronDown, ChevronUp } from 'lucide-react';\n\nimport {\n  createContext,\n  type CSSProperties,\n  forwardRef,\n  type HTMLAttributes,\n  isValidElement,\n  type KeyboardEvent,\n  type MouseEvent,\n  type ReactElement,\n  type ReactNode,\n  type TdHTMLAttributes,\n  type ThHTMLAttributes,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n} from 'react';\nimport { cn } from '@/lib/utils';\nimport { Button } from '@/components/ui/button';\nimport { EmptyState, type EmptyStateProps } from '@/components/ui/empty-state';\nimport { LoadingSpinner } from '@/components/ui/loading-spinner';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { edgeClass, useEdge } from '@/components/ui/edge';\nimport { useLabels, usePreset } from '@/components/ui/labels-context';\n\n// `zero` and `zero-compact` follow the Figma DataGrid: head 34px (`table-head`), rows 44px (`row`) or 36px (`row-compact`), 12px cell padding.\ntype TableDensity = 'compact' | 'default' | 'comfortable' | 'zero' | 'zero-compact';\ntype SortDirection = 'asc' | 'desc' | false;\n// Where a sticky header sticks. `container` (default) clamps the table height and scrolls inside it; `parent` fills a parent that has a height; `page` sticks under the app top bar while the page scrolls.\ntype TableStickyMode = 'container' | 'parent' | 'page';\n\ninterface TableContextValue {\n  density?: TableDensity;\n  striped?: boolean;\n  stickyHeader?: boolean;\n  stickyMode?: TableStickyMode;\n  stickyOffset?: string;\n  headerTone?: TableHeaderTone;\n  rowHover?: TableRowHover;\n}\n\n// `muted` paints the header band with the muted surface; `table` hovers rows with the dedicated table hover token.\ntype TableHeaderTone = 'default' | 'muted';\ntype TableRowHover = 'default' | 'table';\n\nconst TableContext = createContext<TableContextValue>({\n  density: 'default',\n  striped: false,\n  stickyHeader: true,\n  stickyMode: 'container',\n  headerTone: 'default',\n  rowHover: 'default',\n});\n\n// Every color below is one of the dedicated `table-*` tokens (Design Tokens library, live —\n// `colors/table/background/default`, `/selected`, `colors/table/foreground/default`) or an\n// existing generic semantic/`general-*` token already used the same way elsewhere in this repo\n// (Chip, Alert). None is a raw Tailwind palette class.\nconst tableVariants = cva('w-full caption-bottom text-sm bg-background text-foreground');\n\ninterface TableProps extends HTMLAttributes<HTMLTableElement>, VariantProps<typeof tableVariants> {\n  // Row and header sizing. Unset: `zero` in the zero preset, otherwise `default`.\n  density?: TableDensity;\n  striped?: boolean;\n  stickyHeader?: boolean;\n  // Sticky behaviour: `container` keeps the bounded scroll area (default), `parent` fills a parent with a set height, `page` sticks the header under the top bar (`top-topbar`) while the page scrolls. `page` does not scroll horizontally inside the table.\n  stickyMode?: TableStickyMode;\n  // `page` mode only: distance from the viewport top where the header sticks, as a CSS length or var, e.g. `calc(var(--size-topbar) + var(--size-toolbar))`.\n  // Stacks the header below a sticky toolbar. Sets `--table-sticky-offset` on the header, which defaults to `var(--size-topbar)`.\n  // You can also set `--table-sticky-offset` on any ancestor instead.\n  stickyOffset?: string;\n  /** Header band colour. `default` keeps the surface colour; `muted` uses the muted surface and subtle text. */\n  headerTone?: TableHeaderTone;\n  /** Row hover colour. `default` is the muted wash; `table` uses `table-background-hover`. */\n  rowHover?: TableRowHover;\n  // Height cap of the scroll area as a CSS length or var, for example `600px` or `60vh`. Opt-in: without it the table grows with its rows. The cap makes the table scroll vertically inside itself, which is what the sticky header sticks to.\n  // For a sticky header inside a scroll container you provide, use `stickyMode=\"page\"` with `stickyOffset=\"0px\"` (the table then does not scroll sideways by itself).\n  maxHeight?: string;\n  /** Adds the `elevation-table` shadow to the table container (zero theme: none in base). */\n  elevated?: boolean;\n}\n\nconst Table = forwardRef<HTMLTableElement, TableProps>(\n  (\n    {\n      className,\n      density: densityProp,\n      striped = false,\n      stickyHeader = true,\n      stickyMode = 'container',\n      stickyOffset,\n      headerTone = 'default',\n      rowHover = 'default',\n      elevated = false,\n      maxHeight,\n      ...props\n    },\n    ref,\n  ) => {\n    const zeroPreset = usePreset() === 'zero';\n    // The zero preset defaults to the zero density (rows 44, head 34, cell padding 12), other presets to `default`. An explicit density always wins.\n    const density: TableDensity = densityProp ?? (zeroPreset ? 'zero' : 'default');\n    // Horizontally scrolled: pinned columns show their edge shadow only then.\n    const [scrolledX, setScrolledX] = useState(false);\n    // A scroll area with nothing focusable inside must be reachable by keyboard (WCAG 2.1.1), so it becomes a tab stop only while it really overflows.\n    const wrapperRef = useRef<HTMLDivElement | null>(null);\n    const [overflowing, setOverflowing] = useState(false);\n    useEffect(() => {\n      const node = wrapperRef.current;\n      if (!node) return;\n      const measure = () => setOverflowing(node.scrollWidth > node.clientWidth || node.scrollHeight > node.clientHeight);\n      measure();\n      if (typeof ResizeObserver === 'undefined') return;\n      const observer = new ResizeObserver(measure);\n      observer.observe(node);\n      if (node.firstElementChild) observer.observe(node.firstElementChild);\n      return () => observer.disconnect();\n    }, []);\n    return (\n    <TableContext.Provider value={{ density, striped, stickyHeader, stickyMode, stickyOffset, headerTone, rowHover }}>\n      {/* `overflow-x-auto` is unconditional so a wide table always scrolls within its own bounded\n          container (WCAG 1.4.10 Reflow) regardless of `stickyHeader`, instead of overflowing the\n          page. Only the vertical clamp (`max-h`/`overflow-y-auto`) is gated on `stickyHeader`,\n          since that's the one feature that needs a bounded viewport to scroll under. `rounded-default`\n          + the same `border-border-subtle` token the row/thead dividers already use gives Table a\n          real, clipped surface boundary like every other surface component (Card/Dialog/Alert),\n          instead of flat, borderless corners. */}\n      <div\n        ref={wrapperRef}\n        tabIndex={overflowing ? 0 : undefined}\n        data-scrolled-x={scrolledX || undefined}\n        style={maxHeight && stickyHeader && stickyMode === 'container' ? { maxHeight } : undefined}\n        onScroll={(event) => {\n          const next = event.currentTarget.scrollLeft > 0;\n          if (next !== scrolledX) setScrolledX(next);\n        }}\n        className={cn(\n          'relative w-full rounded-surface border border-solid focus-visible:outline-none focus-visible:ring-focus focus-visible:ring-inset focus-visible:ring-ring-default',\n          edgeClass(zeroPreset),\n          // Page mode must not clip: an overflow container would become the sticky scroll parent.\n          stickyHeader && stickyMode === 'page' ? 'overflow-visible' : 'overflow-x-auto',\n          stickyHeader && stickyMode === 'container' && maxHeight && 'overflow-y-auto',\n          stickyHeader && stickyMode === 'parent' && 'h-full min-h-0 overflow-y-auto',\n          elevated && 'shadow-elevation-table',\n        )}\n      >\n        <table ref={ref} className={cn(tableVariants({ className }))} {...props} />\n      </div>\n    </TableContext.Provider>\n    );\n  },\n);\nTable.displayName = 'Table';\n\nconst TableHeader = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>(\n  ({ className, style, ...props }, ref) => {\n    const { stickyHeader, stickyMode, stickyOffset, headerTone } = useContext(TableContext);\n    const edge = useEdge();\n    return (\n      <thead\n        ref={ref}\n        style={stickyOffset && stickyMode === 'page' ? ({ '--table-sticky-offset': stickyOffset, ...style } as CSSProperties) : style}\n        className={cn(\n          '[&_tr]:border-b border-solid',\n          // A muted header paints the row accent gutter (border-l-4) in the header fill, so no strip of table surface shows next to the first header cell.\n          headerTone === 'muted' && '[&_tr]:border-l-muted',\n          edge,\n          // NOTE: `bg-table-background-default` replaces a previously hardcoded `bg-white` here —\n          // that raw value stayed white in dark mode, occluding scrolled-under rows with a\n          // light panel over a dark table. Token-driven, it now tracks the theme in both modes.\n          stickyHeader &&\n            `sticky ${stickyMode === 'page' ? 'top-[var(--table-sticky-offset,var(--size-topbar))]' : 'top-0'} z-sticky ${\n              headerTone === 'muted' ? 'bg-muted' : 'bg-background'\n            } shadow-[0_1px_0_0] shadow-border-muted`,\n          className,\n        )}\n        {...props}\n      />\n    );\n  },\n);\nTableHeader.displayName = 'TableHeader';\n\nconst TableBody = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>(\n  ({ className, ...props }, ref) => {\n    const { striped } = useContext(TableContext);\n    return (\n      <tbody\n        ref={ref}\n        className={cn(\n          '[&_tr:last-child]:border-b-0',\n          // Static zebra-stripe surface — `background-subtle` (a permanent-surface token), not the\n          // pointer-state `general-background-hover` token, even though both currently resolve to\n          // the same slate-50 value in light mode. The `:not(...)` guards scope the stripe selector\n          // to rows that are neither selected nor invalid, so a row's own state tint (set via\n          // `data-state=\"selected\"` / `aria-invalid`, see `tableRowVariants`) is never silently\n          // overridden by the zebra tint on even rows — the state cue stays visible for sighted\n          // users, not just `aria-selected`/`aria-invalid` for assistive tech.\n          striped && '[&_tr:nth-child(even):not([data-state=\"selected\"]):not([aria-invalid=\"true\"])]:bg-background-neutral-subtle [&_tr:nth-child(even):not([data-state=\"selected\"]):not([aria-invalid=\"true\"])]:[--table-pin-tint:theme(colors.background-neutral-subtle)]',\n          className,\n        )}\n        {...props}\n      />\n    );\n  },\n);\nTableBody.displayName = 'TableBody';\n\nconst TableFooter = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>(\n  ({ className, ...props }, ref) => {\n    const edge = useEdge();\n    return (\n    <tfoot\n      ref={ref}\n      className={cn(\n        // `border-border-subtle` replaces a previously colorless `border-t` (no color utility was\n        // set, so the browser's UA-default border color rendered instead — visually undefined and\n        // very likely wrong in dark mode). `bg-background-neutral-subtle` replaces raw `bg-slate-50`.\n        'border-t border-solid bg-muted/50 font-medium [&>tr]:last:border-b-0',\n        edge,\n        className,\n      )}\n      {...props}\n    />\n    );\n  },\n);\nTableFooter.displayName = 'TableFooter';\n\nexport interface TableRowProps extends HTMLAttributes<HTMLTableRowElement> {\n  /**\n   * Passing `onClick` opts the row into the full interactive bundle: hover/pressed state layer,\n   * a real tab stop, a `focus-visible` ring, `cursor-pointer`, and keyboard activation on both\n   * `Enter` and `Space`. A screen-reader user tabbing onto the row is only told \"row\" by its\n   * native semantics — pass a descriptive `aria-label` (e.g. `aria-label=\"Open Laptop Pro\"`) so\n   * the row's accessible name communicates what activating it does (WCAG 4.1.2).\n   */\n  onClick?: HTMLAttributes<HTMLTableRowElement>['onClick'];\n  /**\n   * Marks this row as the current selection (e.g. a checked/active record). Sets\n   * `data-state=\"selected\"` for styling (kept as the pre-existing DOM-attribute contract) and\n   * `aria-selected` for assistive tech — `row` has native ARIA-table semantics and supports\n   * `aria-selected` directly, unlike Chip's plain `<div>`, so no extra sr-only text is needed here.\n   */\n  selected?: boolean;\n  /**\n   * Dims the row and blocks pointer interaction. `<tr>` has no native `disabled` attribute, so\n   * this is expressed via `aria-disabled`/`data-disabled` — same rationale as Chip/Avatar's opt-in\n   * interactive mode.\n   */\n  disabled?: boolean;\n  /**\n   * Flags the row as failing validation (e.g. a bulk-import row that failed a check). Tints the\n   * row with the same `general-error-default` wash Alert's `destructive` variant uses (via the\n   * `/10` opacity modifier — see alert.tsx) and sets `aria-invalid`/`data-invalid`. `aria-invalid`\n   * support is best-effort on a non-widget `row` in some assistive tech — pair with an explicit\n   * status cell/icon column for a cue that is always announced.\n   */\n  invalid?: boolean;\n  // Marks the row whose record is open in a side sheet (Figma Table cell State=Active): brand-subtle wash plus a brand left\n  // accent. Sets `aria-current` and `data-active`. Opt-in; rows without it are unchanged.\n  active?: boolean;\n}\n\nconst tableRowVariants = cva(\n  [\n    // `border-l-4 border-l-transparent` reserves a left-accent gutter on every row (so a row\n    // gaining/losing the selected/invalid accent never shifts layout), painted over by the\n    // selected/invalid compound variants below. It's a shape/position cue in addition to the\n    // background tint — not color alone — for WCAG 1.4.1.\n    'border-b border-l-4 border-solid border-l-transparent transition-colors',\n    // Rows highlight on hover (data tables are scanned); selected/invalid rows keep their own tint.\n    '[&:not([data-state=selected]):not([aria-invalid=true])]:hover:bg-muted/50',\n    // Row state colours as variables, so a pinned (solid) cell can repaint exactly the tint its row shows.\n    '[&:not([data-state=selected]):not([aria-invalid=true])]:hover:[--table-pin-tint:theme(colors.muted/50%)]',\n    'focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/30',\n    'aria-disabled:pointer-events-none aria-disabled:opacity-disabled',\n    'data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-disabled',\n  ],\n  {\n    variants: {\n      selected: { true: '', false: '' },\n      invalid: { true: '', false: '' },\n      // Only an actionable row (a consumer wired `onClick` on, and it's not disabled) gets a\n      // hover/pressed state layer — a plain, non-clickable row (the common case: `Default`,\n      // `CompactDensity`, `Striped`, …) must not show a false clickable affordance.\n      interactive: { true: '', false: '' },\n    },\n    compoundVariants: [\n      {\n        selected: false,\n        invalid: false,\n        interactive: true,\n        className: 'active:bg-muted active:[--table-pin-tint:theme(colors.muted)]',\n      },\n      // Selected wins over invalid when a row is somehow both — the user's explicit selection is\n      // the higher-priority signal; `aria-invalid`/`data-invalid` stay set either way so the\n      // validation flag is never silently lost, only its background tint is deprioritized.\n      // `border-l-general-brand-default` is the non-color (shape/position) cue required alongside\n      // the background tint — see TABLE-A11Y-03 token gap below re: the tint's own contrast ratio.\n      {\n        selected: true,\n        className:\n          'bg-accent hover:bg-accent active:bg-accent [--table-pin-tint:theme(colors.accent)]',\n      },\n      {\n        selected: false,\n        invalid: true,\n        className:\n          'border-l-general-error-default bg-general-error-default/10 hover:bg-general-error-default/20 active:bg-general-error-default/25 [--table-pin-tint:theme(colors.general-error-default/10%)] hover:[--table-pin-tint:theme(colors.general-error-default/20%)] active:[--table-pin-tint:theme(colors.general-error-default/25%)] [--table-pin-accent:theme(colors.general-error-default)]',\n      },\n    ],\n    defaultVariants: { selected: false, invalid: false, interactive: false },\n  },\n);\n\nconst TableRow = forwardRef<HTMLTableRowElement, TableRowProps>(\n  ({ className, selected, disabled, invalid, active, onClick, onKeyDown, tabIndex, ...props }, ref) => {\n    // Only an actionable row (one a consumer wired `onClick` on) becomes a real tab stop — a\n    // purely presentational row never gets an unreachable focus ring (same rule as Chip/Avatar).\n    const interactive = Boolean(onClick) && !disabled;\n    const { rowHover } = useContext(TableContext);\n    const edge = useEdge();\n\n    const handleKeyDown = (event: KeyboardEvent<HTMLTableRowElement>) => {\n      onKeyDown?.(event);\n      // Ignore keys bubbling from nested controls (checkbox, button, link); only the row itself activates.\n      if (!interactive || event.defaultPrevented || event.target !== event.currentTarget) return;\n      // Enter and Space are both wired — the conventional button-activation key set — so keyboard\n      // users get one consistent expectation of what activates a row, matching the \"hover, focus-\n      // visible ring, active press, keyboard-activate\" bundle consumers opt into via `onClick`.\n      if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {\n        event.preventDefault();\n        onClick?.(event as unknown as MouseEvent<HTMLTableRowElement>);\n      }\n    };\n\n    return (\n      <tr\n        ref={ref}\n        data-state={selected ? 'selected' : undefined}\n        data-disabled={disabled || undefined}\n        data-invalid={invalid || undefined}\n        data-active={active || undefined}\n        aria-current={active ? 'true' : undefined}\n        aria-selected={selected || undefined}\n        aria-disabled={disabled || undefined}\n        aria-invalid={invalid || undefined}\n        tabIndex={disabled ? undefined : (tabIndex ?? (interactive ? 0 : undefined))}\n        onClick={disabled ? undefined : onClick}\n        onKeyDown={handleKeyDown}\n        className={cn(\n          // The edge colour comes first so the left accent colours below are not merged away.\n          edge,\n          tableRowVariants({ selected: Boolean(selected), invalid: Boolean(invalid), interactive }),\n          interactive && 'cursor-pointer',\n          active && !selected && !invalid && 'border-l-border-brand bg-background-brand-subtle [--table-pin-tint:theme(colors.background-brand-subtle)] [--table-pin-accent:theme(colors.border-brand)]',\n          rowHover === 'table' &&\n            '[&:not([data-state=selected]):not([aria-invalid=true])]:hover:bg-table-background-hover [&:not([data-state=selected]):not([aria-invalid=true])]:hover:[--table-pin-tint:theme(colors.table-background-hover)]',\n          className,\n        )}\n        {...props}\n      />\n    );\n  },\n);\nTableRow.displayName = 'TableRow';\n\n// Type comes from the dedicated `typography-table-head` role (size/weight/line-height/letter-\n// spacing, incl. its 700 weight — not a hardcoded `font-medium`), never a raw `text-xs`/`text-sm`\n// utility. Density only varies row height and padding, not the type role itself; if density-scaled\n// type ever becomes a real product need, that's a new density-specific type role to raise with\n// design-tokens-maintainer, not a raw size override here.\nconst tableHeadVariants = cva(\n  'typography-table-head text-left align-middle text-muted-foreground [&:has([role=checkbox])]:pr-0',\n  {\n    variants: {\n      density: {\n        compact: 'h-6 px-2 py-1',\n        default: 'h-8 px-4 py-2',\n        comfortable: 'h-12 px-6 py-3',\n        zero: 'h-table-head px-3 py-2',\n        'zero-compact': 'h-table-head px-3 py-2',\n      },\n    },\n    defaultVariants: {\n      density: 'default',\n    },\n  },\n);\n\n// Pinned (sticky) first column. Solid page background plus the row's own tint (--table-pin-tint, set per row state) so hover, selected, invalid, active and striped rows stay opaque,\n// an inset gutter in the row's accent colour (--table-pin-accent) because the row border scrolls away, and an edge shadow that only shows while the table is scrolled sideways.\nconst tablePinned = [\n  'sticky left-0 z-raised bg-background',\n  '[background-image:linear-gradient(var(--table-pin-tint,transparent),var(--table-pin-tint,transparent))]',\n  '[box-shadow:inset_theme(borderWidth.4)_0_0_0_var(--table-pin-accent,transparent)]',\n  'after:pointer-events-none after:absolute after:inset-y-0 after:left-full after:w-3 after:bg-gradient-to-r after:from-overlay-scrim after:to-transparent after:opacity-0 after:transition-opacity',\n  '[[data-scrolled-x=true]_&]:after:opacity-100',\n];\n\n// Select (checkbox) column width from the `table-select` size token; the preset has no width utility for it yet.\nconst tableSelectCell = 'w-[var(--size-table-select)] min-w-[var(--size-table-select)] pr-0';\n\n// Zero densities already reserve the 4px accent gutter on the row, so the checkbox pads 12 to land 16 from the frame edge.\nconst selectCellPadding = (density: TableDensity | undefined, zero: boolean) => (zero && (density === 'zero' || density === 'zero-compact') ? 'pl-3' : 'pl-4');\n\ninterface TableHeadProps extends Omit<ThHTMLAttributes<HTMLTableCellElement>, 'align'> {\n  // Horizontal alignment of the label (and the sort button). `right` suits numbers. Default `left` adds no class.\n  align?: 'left' | 'center' | 'right';\n  /** Fixes the cell to the select-column width (`size-table-select`) for a leading checkbox. */\n  select?: boolean;\n  // Pins the column to the start edge while the table scrolls sideways (use on the head and on every cell of that column).\n  pinned?: boolean;\n  sortable?: boolean;\n  sortDirection?: SortDirection;\n  onSort?: () => void;\n}\n\nconst TableHead = forwardRef<HTMLTableCellElement, TableHeadProps>(\n  ({ className, children, select = false, pinned = false, align, sortable = false, sortDirection = false, onSort, ...props }, ref) => {\n    const { density, headerTone } = useContext(TableContext);\n    const zeroPreset = usePreset() === 'zero';\n    const l = useLabels();\n    // A select column head has no visible text, so its name becomes visually hidden text (an empty table header fails WCAG 1.3.1).\n    const emptySelectHead = select && !sortable && (children === undefined || children === null || children === false || children === '');\n    const ariaSort = !sortable\n      ? undefined\n      : sortDirection === 'asc'\n      ? 'ascending'\n      : sortDirection === 'desc'\n      ? 'descending'\n      : 'none';\n\n    return (\n      <th\n        ref={ref}\n        scope=\"col\"\n        aria-sort={ariaSort}\n        className={cn(\n          tableHeadVariants({ density }),\n          headerTone === 'muted' && 'bg-muted text-text-muted',\n          pinned && ['sticky left-0 z-raised', headerTone === 'muted' ? 'bg-muted' : 'bg-background', tablePinned[3], tablePinned[4]],\n          select && [tableSelectCell, selectCellPadding(density, zeroPreset)],\n          align === 'right' && 'text-right',\n          align === 'center' && 'text-center',\n          className,\n        )}\n        {...props}\n      >\n        {sortable ? (\n          // A real <button> gets keyboard operability (Enter/Space, tab stop) for free — the\n          // previous `onClick`-only <th> had no keyboard path at all (WCAG 2.1.1). Hover/active/\n          // focus-visible are scoped to the button itself, not the whole header cell.\n          <button\n            type=\"button\"\n            onClick={onSort}\n            className={cn(\n              '-mx-1 flex min-h-target w-full cursor-pointer select-none items-center gap-1 rounded-item px-1 transition-colors',\n              align === 'right' && 'justify-end',\n              align === 'center' && 'justify-center',\n              'hover:bg-muted hover:text-foreground active:bg-muted-strong',\n              'focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/30',\n            )}\n          >\n            {children}\n            <span className={align === 'right' || align === 'center' ? undefined : 'ml-auto'} aria-hidden=\"true\">\n              {sortDirection === 'asc' && <ChevronUp className=\"h-4 w-4\" />}\n              {sortDirection === 'desc' && <ChevronDown className=\"h-4 w-4\" />}\n              {!sortDirection && <ChevronUp className=\"h-4 w-4 opacity-20\" />}\n            </span>\n          </button>\n        ) : emptySelectHead ? (\n          <span className=\"sr-only\">{props['aria-label'] ?? l.selectAllRows}</span>\n        ) : (\n          <div className={cn('flex items-center gap-1', align === 'right' && 'justify-end', align === 'center' && 'justify-center')}>{children}</div>\n        )}\n      </th>\n    );\n  },\n);\nTableHead.displayName = 'TableHead';\n\n// Same rationale as `tableHeadVariants` above: type comes from the dedicated `typography-table-cell`\n// role, density only varies padding.\nconst tableCellVariants = cva('typography-table-cell align-middle [&:has([role=checkbox])]:pr-0', {\n  variants: {\n    density: {\n      compact: 'px-2 py-1',\n      default: 'px-4 py-2',\n      comfortable: 'px-6 py-3',\n      zero: 'h-row px-3 py-2',\n      'zero-compact': 'h-row-compact px-3 py-1',\n    },\n  },\n  defaultVariants: {\n    density: 'default',\n  },\n});\n\ninterface TableCellProps extends Omit<TdHTMLAttributes<HTMLTableCellElement>, 'align'> {\n  /** Fixes the cell to the select-column width (`size-table-select`) for a leading checkbox. */\n  select?: boolean;\n  // Pins the column to the start edge while the table scrolls sideways. Opaque in every row state; an edge shadow shows while scrolled.\n  pinned?: boolean;\n  // Horizontal alignment (Figma Align). `right` suits numbers. Default `left` adds no class.\n  align?: 'left' | 'right';\n  // Fixed cell height (Figma Height): `default` is the row height, `two-line` the two-line row height. Unset keeps the density-driven height.\n  rowHeight?: 'default' | 'two-line';\n  // Standalone selected wash (Figma State=Selected). Inside a `TableRow selected` the row already tints.\n  selected?: boolean;\n  // Row open in a side sheet (Figma State=Active): brand-subtle wash and a 2px brand left border.\n  active?: boolean;\n  // Cell is being edited (Figma State=Focused): 2px inside focus ring.\n  focused?: boolean;\n  // Cell failed validation (Figma State=Error): destructive inside border and text. Sets `aria-invalid`.\n  invalid?: boolean;\n  // Dims the cell (Figma State=Disabled).\n  disabled?: boolean;\n  // Chip or badge rendered after the content (Figma Tag).\n  tag?: ReactNode;\n}\n\nconst cellRowHeight = { default: 'h-row', 'two-line': 'h-row-two-line' } as const;\n\nconst TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(\n  (\n    { className, select = false, pinned = false, align, rowHeight, selected, active, focused, invalid, disabled, tag, children, ...props },\n    ref,\n  ) => {\n    const { density } = useContext(TableContext);\n    const zeroPreset = usePreset() === 'zero';\n    return (\n      <td\n        ref={ref}\n        aria-invalid={invalid || undefined}\n        aria-disabled={disabled || undefined}\n        data-state={selected ? 'selected' : undefined}\n        data-active={active || undefined}\n        className={cn(\n          tableCellVariants({ density }),\n          pinned && tablePinned,\n          select && [tableSelectCell, selectCellPadding(density, zeroPreset)],\n          align === 'right' && 'text-right',\n          rowHeight && cellRowHeight[rowHeight],\n          selected && 'bg-table-background-selected',\n          active && 'border-l-2 border-solid border-l-border-brand bg-background-brand-subtle',\n          focused && 'ring-2 ring-inset ring-border-focus',\n          invalid && 'text-text-destructive ring-1 ring-inset ring-border-destructive',\n          disabled && 'pointer-events-none opacity-disabled',\n          className,\n        )}\n        {...props}\n      >\n        {tag ? (\n          <div className={cn('flex items-center gap-2', align === 'right' && 'justify-end')}>\n            {children}\n            {tag}\n          </div>\n        ) : (\n          children\n        )}\n      </td>\n    );\n  },\n);\nTableCell.displayName = 'TableCell';\n\n// Marks a component as rendering a table cell (a `<td>`), so containers such as DataGrid can use it as the cell itself instead of wrapping it in another cell.\nconst TABLE_CELL_MARK = '__tableCell';\n\nfunction markTableCell<C extends object>(component: C): C {\n  (component as Record<string, unknown>)[TABLE_CELL_MARK] = true;\n  return component;\n}\n\nfunction isTableCellElement(node: unknown): node is ReactElement<TableCellProps> {\n  if (!isValidElement(node)) return false;\n  const type = node.type as unknown as Record<string, unknown>;\n  return (typeof type === 'object' || typeof type === 'function') && type !== null && type[TABLE_CELL_MARK] === true;\n}\nmarkTableCell(TableCell);\n\ninterface TableBadgeCellProps extends Omit<TableCellProps, 'tag'> {\n  // `default` pads with space/3 (Figma Style=Badge default), `wide` with space/6 (Style=Badge).\n  padding?: 'default' | 'wide';\n}\n\n// Cell that holds a single Chip or Badge. Compose the chip as children, e.g. `<TableBadgeCell><Chip variant=\"success\">Active</Chip></TableBadgeCell>`.\nconst TableBadgeCell = forwardRef<HTMLTableCellElement, TableBadgeCellProps>(\n  ({ className, padding = 'default', ...props }, ref) => (\n    <TableCell ref={ref} className={cn(padding === 'wide' ? 'px-6' : 'px-3', className)} {...props} />\n  ),\n);\nTableBadgeCell.displayName = 'TableBadgeCell';\nmarkTableCell(TableBadgeCell);\n\ninterface TableLeadCellProps extends Omit<TableCellProps, 'children' | 'tag'> {\n  // Logo or avatar slot, e.g. a `LogoTile` or a square `Avatar size=\"xs\"`. Decorative: the name carries the meaning.\n  logo?: ReactNode;\n  // Leading control slot (checkbox, radio, toggle), rendered before the logo and exposed to assistive tech.\n  control?: ReactNode;\n  name: ReactNode;\n  supportingText?: ReactNode;\n  // Chip after the name, e.g. a Required chip (Figma Lead text required).\n  tag?: ReactNode;\n  // Weight of the name. `medium` (default, Figma Lead text strong) or `regular` (Figma Lead text, 13/400).\n  weight?: 'medium' | 'regular';\n}\n\n// Lead cell: optional control and logo/avatar, strong name, optional supporting line. Same cell contract as TableCell.\nconst TableLeadCell = forwardRef<HTMLTableCellElement, TableLeadCellProps>(\n  ({ className, logo, control, name, supportingText, tag, weight = 'medium', ...props }, ref) => (\n    <TableCell ref={ref} className={className} {...props}>\n      <div className=\"flex items-center gap-2\">\n        {control && <span className=\"inline-flex shrink-0\">{control}</span>}\n        {logo && (\n          <span aria-hidden=\"true\" className=\"inline-flex shrink-0\">\n            {logo}\n          </span>\n        )}\n        <div className=\"flex min-w-0 flex-col\">\n          <span className={cn('truncate text-text-default', weight === 'regular' ? 'typography-ui' : 'typography-ui-medium')}>{name}</span>\n          {supportingText && (\n            <span className=\"typography-text-extra-small truncate text-text-subtle [[data-state=selected]_&]:text-text-subtle-on-muted [[data-active=true]_&]:text-general-brand-on-container\">{supportingText}</span>\n          )}\n        </div>\n        {tag}\n      </div>\n    </TableCell>\n  ),\n);\nTableLeadCell.displayName = 'TableLeadCell';\nmarkTableCell(TableLeadCell);\n\nconst TableCaption = forwardRef<HTMLTableCaptionElement, HTMLAttributes<HTMLTableCaptionElement>>(\n  ({ className, ...props }, ref) => (\n    <caption ref={ref} className={cn('typography-table-item mt-4 text-text-subtle', className)} {...props} />\n  ),\n);\nTableCaption.displayName = 'TableCaption';\n\n// Loading state — composes the shared `Skeleton` primitive instead of hand-rolled pulse `div`s, and\n// announces the loading state to assistive tech (previously purely visual, with no `role`/`aria-live`\n// at all — a real gap given `data-app-ux`'s requirement that loading be a distinguishable, perceivable\n// state, not just a visual one).\ninterface TableLoadingProps extends HTMLAttributes<HTMLDivElement> {\n  columns: number;\n  rows?: number;\n  // `tile` (default) keeps the bordered Skeleton tiles; `row` renders the Figma skeleton rows (overlay-skeleton tokens, row height).\n  skeleton?: 'tile' | 'row';\n}\n\nconst TableLoading = forwardRef<HTMLDivElement, TableLoadingProps>(({ className, columns, rows = 5, skeleton = 'tile', ...props }, ref) => {\n  const l = useLabels();\n  return skeleton === 'row' ? (\n    <Skeleton ref={ref} variant=\"row\" rows={rows} label={l.loadingTableData} className={className} {...props} />\n  ) : (\n  <div\n    ref={ref}\n    role=\"status\"\n    aria-live=\"polite\"\n    aria-label={l.loadingTableData}\n    className={cn('w-full', className)}\n    {...props}\n  >\n    <div className=\"space-y-3\">\n      <div className=\"grid gap-4\" style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}>\n        {Array.from({ length: columns }).map((_, i) => (\n          <Skeleton key={i} className=\"h-4\" />\n        ))}\n      </div>\n      {Array.from({ length: rows }).map((_, rowIndex) => (\n        <div key={rowIndex} className=\"grid gap-4\" style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}>\n          {Array.from({ length: columns }).map((_, colIndex) => (\n            <Skeleton key={colIndex} className=\"h-8\" />\n          ))}\n        </div>\n      ))}\n    </div>\n  </div>\n  );\n});\nTableLoading.displayName = 'TableLoading';\n\n// Empty state component\ninterface TableEmptyProps extends Omit<TdHTMLAttributes<HTMLTableCellElement>, 'align' | 'title'> {\n  columns: number;\n  message?: string;\n  // Rich empty state (Figma Example=Empty state): any of these renders the shared EmptyState instead of the plain message.\n  title?: ReactNode;\n  description?: ReactNode;\n  icon?: ReactNode;\n  actions?: ReactNode;\n  variant?: EmptyStateProps['variant'];\n}\n\n// Fixed a pre-existing bug: this was typed/forwarded as `HTMLDivElement` and its callback never\n// even accepted a `ref` param, so the ref was silently dropped — the actual rendered root is a\n// <td> (via TableCell) inside a <tr>. Now correctly typed/forwarded to the <td>. Also replaced\n// `text-muted-foreground`, which is not a token this system defines (so it resolved to no color at\n// all), with the existing `text-text-subtle` token already used by TableCaption above.\nconst TableEmpty = forwardRef<HTMLTableCellElement, TableEmptyProps>(\n  ({ columns, message: messageProp, className, title, description, icon, actions, variant = 'no-results', ...props }, ref) => {\n    const l = useLabels();\n    const message = messageProp ?? l.noDataAvailable;\n    const rich = title !== undefined || description !== undefined || actions !== undefined || icon !== undefined;\n    return (\n      <TableRow>\n        <TableCell ref={ref} colSpan={columns} className={cn(rich ? 'py-12' : 'h-24 text-center', className)} {...props}>\n          {rich ? (\n            <div className=\"mx-auto w-full max-w-lg\">\n              <EmptyState variant={variant} title={title ?? message} description={description} icon={icon} actions={actions} />\n            </div>\n          ) : (\n            <div className=\"typography-table-item flex flex-col items-center justify-center gap-1 text-text-subtle\">\n              <p>{message}</p>\n            </div>\n          )}\n        </TableCell>\n      </TableRow>\n    );\n  },\n);\nTableEmpty.displayName = 'TableEmpty';\n\ninterface TableErrorProps extends Omit<TdHTMLAttributes<HTMLTableCellElement>, 'align' | 'title'> {\n  columns: number;\n  title?: ReactNode;\n  description?: ReactNode;\n  // Retry button (Figma Example=Error, \"Try again\"). Omit to hide it.\n  onRetry?: () => void;\n  retryLabel?: string;\n}\n\n// Error state row (Figma Example=Error): shared EmptyState error variant, announced as an alert, with a retry action.\nconst TableError = forwardRef<HTMLTableCellElement, TableErrorProps>(\n  (\n    { columns, title: titleProp, description: descriptionProp, onRetry, retryLabel: retryLabelProp, className, ...props },\n    ref,\n  ) => {\n    const l = useLabels();\n    const title = titleProp ?? l.tableErrorTitle;\n    const description = descriptionProp ?? l.tableErrorDescription;\n    const retryLabel = retryLabelProp ?? l.tryAgain;\n    return (\n    <TableRow>\n      <TableCell ref={ref} colSpan={columns} className={cn('py-12', className)} {...props}>\n        <div className=\"mx-auto w-full max-w-lg\">\n          <EmptyState\n            variant=\"error\"\n            title={title}\n            description={description}\n            actions={\n              onRetry ? (\n                <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={onRetry}>\n                  {retryLabel}\n                </Button>\n              ) : undefined\n            }\n          />\n        </div>\n      </TableCell>\n    </TableRow>\n  );\n  },\n);\nTableError.displayName = 'TableError';\n\ninterface TableLoadingMoreProps extends Omit<TdHTMLAttributes<HTMLTableCellElement>, 'align'> {\n  columns: number;\n  // A page is being fetched: shows the spinner and announces `loadingLabel`.\n  loading?: boolean;\n  // Offers a button when idle. Omit for infinite scroll, where only the loading state is shown.\n  onLoadMore?: () => void;\n  label?: string;\n  loadingLabel?: string;\n}\n\n// Footer row for lazy loading (Figma Example=Lazy loading): live status while loading, otherwise a Load more button.\nconst TableLoadingMore = forwardRef<HTMLTableCellElement, TableLoadingMoreProps>(\n  ({ columns, loading = false, onLoadMore, label: labelProp, loadingLabel: loadingLabelProp, className, ...props }, ref) => {\n    const l = useLabels();\n    const label = labelProp ?? l.loadMore;\n    const loadingLabel = loadingLabelProp ?? l.loadingMore;\n    return (\n    <TableRow>\n      <TableCell ref={ref} colSpan={columns} className={cn('text-center', className)} {...props}>\n        <div role=\"status\" aria-live=\"polite\" className=\"flex items-center justify-center gap-3 typography-table-item text-text-subtle\">\n          {loading ? (\n            <>\n              <LoadingSpinner size=\"sm\" role=\"presentation\" aria-live=\"off\" aria-label=\"\" />\n              <span>{loadingLabel}</span>\n            </>\n          ) : (\n            onLoadMore && (\n              <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={onLoadMore}>\n                {label}\n              </Button>\n            )\n          )}\n        </div>\n      </TableCell>\n    </TableRow>\n  );\n  },\n);\nTableLoadingMore.displayName = 'TableLoadingMore';\n\nexport {\n  isTableCellElement,\n  markTableCell,\n  Table,\n  TableBody,\n  TableBadgeCell,\n  TableCaption,\n  TableCell,\n  TableEmpty,\n  TableError,\n  TableFooter,\n  TableHead,\n  TableHeader,\n  TableLeadCell,\n  TableLoading,\n  TableLoadingMore,\n  TableRow,\n  type SortDirection,\n  type TableBadgeCellProps,\n  type TableCellProps,\n  type TableDensity,\n  type TableHeaderTone,\n  type TableRowHover,\n  type TableStickyMode,\n  type TableLeadCellProps,\n  type TableProps,\n};\n\n// --- KNOWN TOKEN GAPS (component-library-maintainer cannot close these from table.tsx — they are\n// color VALUES owned by the design tokens; Table only consumes tokens by name and\n// must not hardcode a raw hex/palette value around them) ---\n//\n// 1. No dedicated `table-border-*` token exists (only `table-background-{default,selected}` and\n//    `table-foreground-default` are published). Row/header/footer dividers above fall back to the\n//    generic semantic `border-default`/`border-muted` tokens (the same tier Card/Input/Checkbox use\n//    for their own component-scoped borders) rather than a hardcoded value. A dedicated\n//    `table-border-default` (and possibly `table-border-muted` for the lighter row-divider vs. a\n//    stronger header/footer rule) would let Table's grid lines be tuned independently of unrelated\n//    surfaces that also read `border-muted`.\n//\n// 2. WCAG 1.4.11 non-text contrast in dark mode (tracks the same root cause already flagged on\n//    Alert/Chip as ALERT-TOK-03 / CHIP-A11Y-05): `--semantic-border-muted` (rgb(30 41 59)) against\n//    `--colors-table-background-default` (rgb(15 23 42)) computes to only ~1.3:1 in the dark theme —\n//    under the 3:1 minimum. Row/header dividers are decorative grid lines rather than an\n//    operable-control boundary, so this is flagged as a hygiene gap rather than a blocker; still\n//    needs a dedicated, more-separated dark-mode value from design-tokens-maintainer if row/column\n//    boundaries ever become load-bearing for comprehension (e.g. a dense financial grid).\n//\n// 3. No `general-*-selected` token distinct from `-hover`/`-active` exists for the invalid-row tint\n//    either (same family of gap as CHIP-MAT-01): a row that is simultaneously `selected` and\n//    `invalid` can only show one tint (see the compoundVariants comment above), because there is no\n//    token to blend or distinguish the two. The left-accent border (`border-l-general-brand-default`)\n//    added below at least keeps a shape-based cue distinct in that combined case, but a dedicated\n//    combined \"selected + invalid\" background treatment would need new token support, not a\n//    hardcoded color, to add.\n//\n// 4. TABLE-A11Y-01 (blocker): `colors/table/background/default`, `colors/table/background/selected`,\n//    and `colors/table/foreground/default` are published with a `:root`-only (light) value in\n//    `kette-tokens/dist/theme.css` — neither the `.dark` block nor\n//    `@media (prefers-color-scheme: dark)` overrides any of the three, so in dark mode Table\n//    renders a light/near-black surface island against otherwise-dark chrome. This is a token\n//    VALUE gap in the design tokens (`styles/theme.css`), not something table.tsx can fix by\n//    changing which token name it binds — needs design-tokens-maintainer to add `.dark` values for\n//    all three and verify text-on-surface contrast stays ≥4.5:1 before this can close.\n//\n// 5. TABLE-A11Y-03: the selected/invalid row background tints are the only *color* differentiator\n//    from the default row background (`table-background-selected` vs `table-background-default` is\n//    ~1.10:1; the invalid `general-error-default/10` wash is ~1.17:1 — both under the 3:1 a\n//    state-bearing UI boundary needs). The left-accent border added to `tableRowVariants` below\n//    supplies the required non-color (shape) cue, but the tint values themselves still need to be\n//    raised to ≥3:1 against `table-background-default` by design-tokens-maintainer to also serve as\n//    a secondary cue for low-vision users — table.tsx cannot raise a token's color value itself.\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}