{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"data-table","type":"registry:ui","title":"Data table","description":"Accessible table markup with consistent cell styling and a scrollable region.","dependencies":["clsx","lucide-react","tailwind-merge"],"registryDependencies":["https://caliperui.site/r/styles.json"],"files":[{"path":"ui/data-table.tsx","type":"registry:ui","target":"components/ui/data-table.tsx","content":"import { ArrowDown, ArrowUp, ArrowUpDown } from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/bulk-bar.css\";\nimport \"./shared/chip-option.css\";\nimport \"./data-table.css\";\n\nexport type DataTableProps = ComponentProps<\"table\"> & {\n  caption: string;\n};\n\nexport function DataTable({\n  caption,\n  className,\n  children,\n  ...props\n}: DataTableProps) {\n  return (\n    <div\n      role=\"region\"\n      aria-label={`${caption}, scrollable table`}\n      tabIndex={0}\n      className=\"relative min-w-0 overflow-x-auto focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\"\n    >\n      <table\n        data-slot=\"data-table\"\n        className={cn(\"w-full border-collapse text-start text-sm\", className)}\n        {...props}\n      >\n        <caption className=\"sr-only\">{caption}</caption>\n        {children}\n      </table>\n    </div>\n  );\n}\n\nexport function DataTableHead({\n  className,\n  scope = \"col\",\n  numeric = false,\n  sort,\n  onSort,\n  children,\n  ...props\n}: ComponentProps<\"th\"> & {\n  /** Right-aligned with tabular figures, for money and counts. */\n  numeric?: boolean;\n  /** Makes the header a sort button. */\n  sort?: \"ascending\" | \"descending\" | \"none\";\n  onSort?: () => void;\n}) {\n  const Icon = sort === \"ascending\" ? ArrowUp : sort === \"descending\" ? ArrowDown : ArrowUpDown;\n  return (\n    <th\n      scope={scope}\n      aria-sort={sort}\n      className={cn(\n        \"border-b border-border px-3 text-start first:ps-4 last:pe-4\",\n        scope === \"row\"\n          ? \"py-[var(--row-padding)] align-middle text-sm font-normal\"\n          : \"h-11 text-xs font-medium text-muted-foreground\",\n        numeric && \"text-end tabular-nums\",\n        className,\n      )}\n      {...props}\n    >\n      {onSort ? (\n        <button type=\"button\" data-slot=\"sort-button\" onClick={onSort} className={cn(\"inline-flex items-center gap-1 rounded-[4px] outline-none hover:text-foreground focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\", numeric && \"flex-row-reverse\")}>\n          {children}\n          <Icon aria-hidden=\"true\" className={cn(\"size-3.5\", sort === \"none\" && \"opacity-50\")} />\n        </button>\n      ) : (\n        children\n      )}\n    </th>\n  );\n}\n\nexport function DataTableCell({\n  className,\n  numeric = false,\n  ...props\n}: ComponentProps<\"td\"> & { numeric?: boolean }) {\n  return (\n    <td\n      className={cn(\n        \"border-b border-border px-3 py-[var(--row-padding)] align-middle first:ps-4 last:pe-4\",\n        numeric && \"text-end tabular-nums\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** A row with a quiet hover fill, an ember-tinted selected state and a keyboard cursor. */\nexport function DataTableRow({\n  selected,\n  cursor,\n  className,\n  ...props\n}: ComponentProps<\"tr\"> & { selected?: boolean; cursor?: boolean }) {\n  return (\n    <tr\n      data-slot=\"data-table-row\"\n      aria-selected={selected}\n      data-cursor={cursor || undefined}\n      className={className}\n      {...props}\n    />\n  );\n}\n\n/** Rises from the bottom while rows are selected: the count, the total and actions for all of them. */\nexport function DataTableBulkBar({\n  count,\n  summary,\n  children,\n  className,\n}: {\n  count: number;\n  summary?: ReactNode;\n  children: ReactNode;\n  className?: string;\n}) {\n  return (\n    <div role=\"toolbar\" aria-label=\"Bulk actions\" data-slot=\"bulk-bar\" data-open={count > 0} aria-hidden={count === 0 || undefined} className={className}>\n      <span className=\"text-sm font-medium tabular-nums\" aria-live=\"polite\">\n        {count} selected{summary ? <> · {summary}</> : null}\n      </span>\n      {children}\n    </div>\n  );\n}\n"},{"path":"ui/lib/cn.ts","type":"registry:ui","target":"components/ui/lib/cn.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\nconst twMerge = extendTailwindMerge({\n  extend: { theme: { radius: [\"control\", \"surface\", \"overlay\"], shadow: [\"overlay\"] } },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"},{"path":"ui/shared/bulk-bar.css","type":"registry:file","target":"components/ui/shared/bulk-bar.css","content":"/* Shared by data-table.tsx, team-members.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"bulk-bar\"] {\n    position: sticky;\n    bottom: 16px;\n    z-index: 5;\n    display: flex;\n    width: fit-content;\n    max-width: calc(100% - 16px);\n    margin: 12px auto 0;\n    align-items: center;\n    gap: 8px;\n    padding: 6px 6px 6px 14px;\n    border-radius: var(--radius-surface);\n    background: var(--primary);\n    color: var(--primary-foreground);\n    box-shadow: var(--shadow-overlay);\n    transition:\n      translate 320ms var(--ease-settle),\n      opacity var(--duration-normal) var(--ease-out);\n  }\n  [data-slot=\"bulk-bar\"][data-open=\"false\"] {\n    translate: 0 24px;\n    opacity: 0;\n    pointer-events: none;\n    visibility: hidden;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"bulk-bar\"] {\n      transition: none;\n    }\n  }\n  [data-slot=\"bulk-bar\"] { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; padding: 0.5rem 0.75rem; border: 1px solid var(--border); border-radius: var(--radius-control); background: var(--surface); animation: think-in var(--duration-normal) var(--ease-out) both; }\n  @media (prefers-reduced-motion: reduce) { [data-slot=\"bulk-bar\"] { animation: none; } }\n}\n"},{"path":"ui/shared/chip-option.css","type":"registry:file","target":"components/ui/shared/chip-option.css","content":"/* Shared by calendar.tsx, chart-parts.tsx, chip-group.tsx, data-table.tsx, number-field.tsx, password-field.tsx, stepper.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* Depth sets 2 and 3 end */\n\n  /* Press feedback (2026-10-20, motion audit): every pressable control answers a press. Compact controls take the\n     system press scale, as Button does; wide rows deepen their fill instead, since scaling a full-width row reads as a\n     jump. Disabled controls stay still. */\n  :is([data-slot=\"chip-option\"], [data-slot=\"number-field\"] button, [data-slot=\"password-toggle\"], [data-slot=\"calendar\"] button, [data-slot=\"chart-legend-item\"], [data-slot=\"sort-button\"], [data-slot=\"stepper\"] button):active:not(:disabled):not([aria-disabled=\"true\"]) {\n    scale: var(--motion-press-scale);\n  }\n}\n"},{"path":"ui/data-table.css","type":"registry:file","target":"components/ui/data-table.css","content":"/* data-table.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"data-table\"] tbody tr {\n    transition: background-color var(--duration-fast) var(--ease-out);\n  }\n\n  /* Data table rows: quiet hover, ember-tinted selection, a keyboard cursor. */\n  [data-slot=\"data-table-row\"] > * {\n    transition: background-color var(--duration-fast) var(--ease-out);\n  }\n  @media (hover: hover) {\n    [data-slot=\"data-table-row\"]:hover > * {\n      background: var(--muted);\n    }\n  }\n  [data-slot=\"data-table-row\"][aria-selected=\"true\"] > * {\n    background: var(--selected);\n  }\n  [data-slot=\"data-table-row\"][data-cursor] > :first-child {\n    box-shadow: inset 2px 0 0 var(--foreground);\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"data-table\"] tbody tr {\n      transition: none;\n    }\n  }\n  [data-slot=\"sort-button\"] {\n    display: inline-flex;\n    align-items: center;\n    gap: 0.25rem;\n    margin-inline-start: -0.375rem;\n    padding: 0.25rem 0.375rem;\n    border-radius: var(--radius-control);\n    font: inherit;\n    color: inherit;\n    outline: none;\n    transition:\n      background-color var(--duration-fast) var(--ease-out),\n      color var(--duration-fast) var(--ease-out);\n  }\n  [data-slot=\"sort-button\"][data-align=\"end\"] {\n    margin-inline: auto -0.375rem;\n    flex-direction: row-reverse;\n  }\n  [data-slot=\"sort-button\"]:hover,\n  [data-slot=\"sort-button\"][data-active=\"true\"] {\n    color: var(--foreground);\n  }\n  [data-slot=\"sort-button\"]:hover {\n    background: var(--muted);\n  }\n  [data-slot=\"sort-button\"]:focus-visible {\n    outline: 2px solid var(--ring);\n    outline-offset: 1px;\n  }\n  @media (prefers-reduced-motion: no-preference) {\n    [data-slot=\"data-table-row\"][aria-selected=\"true\"] > * { animation: caliper-select-in var(--duration-normal) var(--ease-out) both; }\n  }\n}\n"}],"categories":["data"],"meta":{"tier":"free","license":"MIT","family":"records","docs":"/docs/components/data-table"}}