{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "type": "registry:ui",
  "title": "Data table",
  "description": "React data table with sortable columns, row selection, a floating bulk actions bar, hover row actions, sticky header, three densities and skeleton loading.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://ui.minidev.pro/r/checkbox.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/data-table.tsx",
      "type": "registry:ui",
      "target": "components/ui/data-table.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { ArrowDownIcon, ArrowUpIcon, ChevronsUpDownIcon, XIcon } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\n\n/** A product data table: sortable headers, row selection with a bulk bar, hover row actions, sticky header, density and skeleton loading. */\n\n/*\n * GOLD STANDARD COMPONENT.\n * A table that feels like a product, not a spreadsheet: sortable headers with\n * aria-sort, row selection with an indeterminate header, a selected-row accent\n * rail, hover-revealed row actions, sticky header that gains a hairline shadow\n * only once you scroll, density, skeleton loading, and a floating bulk bar.\n */\n\nexport type Column<T> = {\n  id: string\n  header: React.ReactNode\n  cell: (row: T) => React.ReactNode\n  /** Enables sorting on this column. Return the value to compare. */\n  sortValue?: (row: T) => string | number | Date | null | undefined\n  align?: \"left\" | \"right\" | \"center\"\n  /** CSS width, e.g. \"30%\" or 120. */\n  width?: number | string\n  className?: string\n}\n\ntype Sort = { id: string; dir: \"asc\" | \"desc\" } | null\ntype Density = \"compact\" | \"default\" | \"comfortable\"\n\n/* Sample rows: Lumen invoices. Used when `columns` and `data` are omitted. */\ntype Invoice = { id: string; customer: string; email: string; amount: number; status: \"paid\" | \"pending\" | \"overdue\" | \"refunded\"; issued: string }\n\nconst INVOICES: Invoice[] = [\n  { id: \"INV-2041\", customer: \"Northwind Labs\", email: \"billing@northwind.io\", amount: 12400, status: \"paid\", issued: \"2026-09-24\" },\n  { id: \"INV-2040\", customer: \"Acme Robotics\", email: \"ap@acme.dev\", amount: 3890, status: \"pending\", issued: \"2026-09-22\" },\n  { id: \"INV-2039\", customer: \"Globex\", email: \"finance@globex.com\", amount: 890, status: \"overdue\", issued: \"2026-09-12\" },\n  { id: \"INV-2038\", customer: \"Initech\", email: \"peter@initech.co\", amount: 5200, status: \"paid\", issued: \"2026-09-10\" },\n  { id: \"INV-2037\", customer: \"Umbrella Health\", email: \"ops@umbrella.health\", amount: 21750, status: \"paid\", issued: \"2026-09-08\" },\n  { id: \"INV-2036\", customer: \"Hooli\", email: \"gavin@hooli.xyz\", amount: 1450, status: \"refunded\", issued: \"2026-09-03\" },\n  { id: \"INV-2035\", customer: \"Stark Industrial\", email: \"pepper@stark.io\", amount: 9980, status: \"pending\", issued: \"2026-08-29\" },\n  { id: \"INV-2034\", customer: \"Wayne Freight\", email: \"lucius@wayne.co\", amount: 6300, status: \"paid\", issued: \"2026-08-27\" },\n]\n\nconst STATUS_DOT: Record<Invoice[\"status\"], string> = { paid: \"bg-success\", pending: \"bg-warning\", overdue: \"bg-danger\", refunded: \"bg-fg-subtle\" }\nconst money = (n: number) => n.toLocaleString(\"en-US\", { style: \"currency\", currency: \"USD\", maximumFractionDigits: 0 })\nconst day = (s: string) => new Date(s + \"T00:00:00\").toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" })\n\nconst INVOICE_COLUMNS: Column<Invoice>[] = [\n  {\n    id: \"customer\",\n    header: \"Customer\",\n    sortValue: (r) => r.customer,\n    cell: (r) => (\n      <div className=\"flex min-w-0 items-center gap-2.5\">\n        <span className=\"grid size-7 shrink-0 place-items-center rounded-full border border-border bg-sunken text-[11px] font-medium text-fg-muted\">\n          {r.customer.split(\" \").map((w) => w[0]).slice(0, 2).join(\"\")}\n        </span>\n        <div className=\"min-w-0\">\n          <p className=\"truncate font-medium text-fg\">{r.customer}</p>\n          <p className=\"truncate text-xs text-fg-subtle\">{r.email}</p>\n        </div>\n      </div>\n    ),\n  },\n  { id: \"id\", header: \"Invoice\", sortValue: (r) => r.id, cell: (r) => <span className=\"font-mono text-xs text-fg-muted\">{r.id}</span> },\n  {\n    id: \"status\",\n    header: \"Status\",\n    sortValue: (r) => r.status,\n    cell: (r) => (\n      <span className=\"inline-flex items-center gap-1.5 capitalize\">\n        <span aria-hidden className={cn(\"size-1.5 rounded-full\", STATUS_DOT[r.status])} />\n        {r.status}\n      </span>\n    ),\n  },\n  { id: \"issued\", header: \"Issued\", sortValue: (r) => r.issued, cell: (r) => <span className=\"text-fg-muted\">{day(r.issued)}</span> },\n  { id: \"amount\", header: \"Amount\", align: \"right\", sortValue: (r) => r.amount, cell: (r) => <span className=\"font-medium\">{money(r.amount)}</span> },\n]\n\n/* The samples again, typed so they can be the defaults of the generic signature for any T. */\nconst SAMPLE_COLUMNS = INVOICE_COLUMNS as Column<unknown>[]\nconst SAMPLE_ROWS = INVOICES as never[]\n\ntype DataTableProps<T> = {\n  /** Column definitions. Defaults, with `data`, to a Lumen invoice table. */\n  columns?: Column<T>[]\n  /** Rows. Defaults, with `columns`, to eight sample invoices. */\n  data?: T[]\n  getRowId?: (row: T, index: number) => string\n  /** Adds a checkbox column. */\n  selectable?: boolean\n  selected?: string[]\n  onSelectedChange?: (ids: string[]) => void\n  /** Rendered in the floating bar while rows are selected. */\n  bulkActions?: (ids: string[]) => React.ReactNode\n  /** Rendered at the row's end, revealed on hover/focus. */\n  rowActions?: (row: T) => React.ReactNode\n  onRowClick?: (row: T) => void\n  defaultSort?: Sort\n  density?: Density\n  loading?: boolean\n  /** Fixes the height and makes the header sticky. */\n  maxHeight?: number | string\n  empty?: React.ReactNode\n  caption?: string\n  className?: string\n}\n\nconst ROW_H: Record<Density, string> = { compact: \"h-9\", default: \"h-11\", comfortable: \"h-14\" }\nconst ALIGN = { left: \"text-left\", right: \"text-right\", center: \"text-center\" }\n\nfunction compare(a: unknown, b: unknown) {\n  if (a == null && b == null) return 0\n  if (a == null) return 1\n  if (b == null) return -1\n  if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime()\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 DataTable<T = Invoice>({\n  columns = SAMPLE_COLUMNS,\n  data = SAMPLE_ROWS,\n  getRowId = (_r, i) => String(i),\n  selectable,\n  selected: selectedProp,\n  onSelectedChange,\n  bulkActions,\n  rowActions,\n  onRowClick,\n  defaultSort = null,\n  density = \"default\",\n  loading,\n  maxHeight,\n  empty,\n  caption,\n  className,\n}: DataTableProps<T>) {\n  const [sort, setSort] = React.useState<Sort>(defaultSort)\n  const [innerSel, setInnerSel] = React.useState<string[]>([])\n  const selected = selectedProp ?? innerSel\n  const setSelected = (ids: string[]) => {\n    if (selectedProp === undefined) setInnerSel(ids)\n    onSelectedChange?.(ids)\n  }\n  const [scrolled, setScrolled] = React.useState(false)\n\n  const rows = React.useMemo(() => {\n    const withIds = data.map((row, i) => ({ row, id: getRowId(row, i) }))\n    const col = sort && columns.find((c) => c.id === sort.id)\n    if (!col?.sortValue) return withIds\n    const sv = col.sortValue\n    const sorted = [...withIds].sort((a, b) => compare(sv(a.row), sv(b.row)))\n    return sort!.dir === \"desc\" ? sorted.reverse() : sorted\n  }, [data, sort, columns, getRowId])\n\n  const ids = rows.map((r) => r.id)\n  const selCount = ids.filter((id) => selected.includes(id)).length\n  const allChecked = ids.length > 0 && selCount === ids.length\n  const someChecked = selCount > 0 && !allChecked\n\n  const toggleSort = (id: string) =>\n    setSort((s) => (s?.id !== id ? { id, dir: \"asc\" } : s.dir === \"asc\" ? { id, dir: \"desc\" } : null))\n\n  const colCount = columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)\n  const pad = density === \"compact\" ? \"px-3\" : \"px-4\"\n\n  return (\n    <div data-slot=\"data-table\" data-density={density} className={cn(\"relative w-full\", className)}>\n      <div\n        className=\"w-full overflow-auto rounded-xl border border-border bg-surface shadow-raised\"\n        style={maxHeight !== undefined ? { maxHeight } : undefined}\n        onScroll={(e) => setScrolled(e.currentTarget.scrollTop > 0)}\n      >\n        <table className=\"w-full border-separate border-spacing-0 text-sm\" aria-busy={loading || undefined}>\n          {caption ? <caption className=\"sr-only\">{caption}</caption> : null}\n          <thead\n            className={cn(\n              \"text-xs font-medium text-fg-muted\",\n              maxHeight !== undefined && \"sticky top-0 z-10\",\n              \"[&_th]:border-b [&_th]:border-border [&_th]:bg-[color-mix(in_oklch,var(--sunken)_70%,var(--surface))]\",\n              scrolled && \"[&_th]:shadow-[0_1px_0_0_var(--border),0_6px_12px_-8px_oklch(0_0_0/0.18)]\"\n            )}\n          >\n            <tr>\n              {selectable ? (\n                <th scope=\"col\" className=\"h-10 w-10 pl-4 text-left\">\n                  <Checkbox\n                    aria-label=\"Select all rows\"\n                    checked={allChecked}\n                    indeterminate={someChecked}\n                    onCheckedChange={() => setSelected(allChecked || someChecked ? selected.filter((s) => !ids.includes(s)) : [...new Set([...selected, ...ids])])}\n                  />\n                </th>\n              ) : null}\n              {columns.map((c) => {\n                const dir = sort?.id === c.id ? sort.dir : null\n                const align = c.align ?? \"left\"\n                return (\n                  <th\n                    key={c.id}\n                    scope=\"col\"\n                    aria-sort={dir ? (dir === \"asc\" ? \"ascending\" : \"descending\") : c.sortValue ? \"none\" : undefined}\n                    style={c.width !== undefined ? { width: c.width } : undefined}\n                    className={cn(\"h-10 font-medium whitespace-nowrap\", pad, ALIGN[align], c.className)}\n                  >\n                    {c.sortValue ? (\n                      <button\n                        type=\"button\"\n                        onClick={() => toggleSort(c.id)}\n                        className={cn(\n                          \"group/sort -mx-1.5 inline-flex h-7 items-center gap-1 rounded-md px-1.5 outline-none\",\n                          \"transition-colors duration-[70ms] hover:bg-fg/5 hover:text-fg focus-visible:ring-2 focus-visible:ring-accent\",\n                          dir && \"text-fg\",\n                          align === \"right\" && \"flex-row-reverse\"\n                        )}\n                      >\n                        {c.header}\n                        <span className=\"grid size-3.5 place-items-center\">\n                          {dir === \"asc\" ? (\n                            <ArrowUpIcon className=\"size-3 text-accent-fg\" />\n                          ) : dir === \"desc\" ? (\n                            <ArrowDownIcon className=\"size-3 text-accent-fg\" />\n                          ) : (\n                            <ChevronsUpDownIcon className=\"size-3 opacity-0 transition-opacity group-hover/sort:opacity-60 group-focus-visible/sort:opacity-60\" />\n                          )}\n                        </span>\n                      </button>\n                    ) : (\n                      c.header\n                    )}\n                  </th>\n                )\n              })}\n              {rowActions ? <th scope=\"col\" className=\"h-10 w-12\"><span className=\"sr-only\">Actions</span></th> : null}\n            </tr>\n          </thead>\n          <tbody className=\"[&>tr:last-child>td]:border-b-0\">\n            {loading ? (\n              Array.from({ length: 5 }, (_, r) => (\n                <tr key={r}>\n                  {Array.from({ length: colCount }, (_, c) => (\n                    <td key={c} className={cn(ROW_H[density], pad, \"border-b border-border\")}>\n                      <span\n                        className=\"block h-2.5 animate-pulse rounded-full bg-sunken\"\n                        style={{ width: `${[62, 44, 78, 36, 55][(r + c) % 5]}%`, animationDelay: `${r * 80}ms` }}\n                      />\n                    </td>\n                  ))}\n                </tr>\n              ))\n            ) : rows.length === 0 ? (\n              <tr>\n                <td colSpan={colCount} className=\"h-40 text-center\">\n                  {empty ?? (\n                    <div className=\"grid place-items-center gap-1\">\n                      <p className=\"text-sm font-medium text-fg\">No results</p>\n                      <p className=\"text-xs text-fg-muted\">Try a different search or clear the filters.</p>\n                    </div>\n                  )}\n                </td>\n              </tr>\n            ) : (\n              rows.map(({ row, id }) => {\n                const isSel = selected.includes(id)\n                return (\n                  <tr\n                    key={id}\n                    data-selected={isSel || undefined}\n                    tabIndex={onRowClick ? 0 : undefined}\n                    onClick={onRowClick ? () => onRowClick(row) : undefined}\n                    onKeyDown={onRowClick ? (e) => { if (e.key === \"Enter\" && e.target === e.currentTarget) onRowClick(row) } : undefined}\n                    className={cn(\n                      \"group/row outline-none transition-colors duration-[70ms]\",\n                      \"[&>td]:border-b [&>td]:border-border\",\n                      \"hover:bg-[color-mix(in_oklch,var(--sunken)_55%,transparent)]\",\n                      \"focus-visible:bg-[color-mix(in_oklch,var(--sunken)_55%,transparent)] focus-visible:[&>td:first-child]:shadow-[inset_2px_0_0_var(--accent)]\",\n                      isSel && \"bg-accent-soft hover:bg-accent-soft [&>td:first-child]:shadow-[inset_2px_0_0_var(--accent)]\",\n                      onRowClick && \"cursor-pointer\"\n                    )}\n                  >\n                    {selectable ? (\n                      <td className={cn(ROW_H[density], \"w-10 pl-4\")} onClick={(e) => e.stopPropagation()}>\n                        <Checkbox\n                          aria-label={`Select row ${id}`}\n                          checked={isSel}\n                          onCheckedChange={(v) => setSelected(v ? [...selected, id] : selected.filter((s) => s !== id))}\n                        />\n                      </td>\n                    ) : null}\n                    {columns.map((c) => (\n                      <td\n                        key={c.id}\n                        className={cn(ROW_H[density], pad, \"text-fg\", ALIGN[c.align ?? \"left\"], c.align === \"right\" && \"tabular-nums\", c.className)}\n                      >\n                        {c.cell(row)}\n                      </td>\n                    ))}\n                    {rowActions ? (\n                      <td className={cn(ROW_H[density], \"w-12 pr-2 text-right\")} onClick={(e) => e.stopPropagation()}>\n                        <div className=\"inline-flex opacity-0 transition-opacity duration-[70ms] group-hover/row:opacity-100 group-focus-within/row:opacity-100 group-data-[selected]/row:opacity-100 [@media(hover:none)]:opacity-100\">\n                          {rowActions(row)}\n                        </div>\n                      </td>\n                    ) : null}\n                  </tr>\n                )\n              })\n            )}\n          </tbody>\n        </table>\n      </div>\n\n      {selectable && selCount > 0 ? (\n        <div\n          role=\"toolbar\"\n          aria-label=\"Bulk actions\"\n          className=\"pointer-events-none sticky bottom-3 z-20 mt-3 flex justify-center\"\n        >\n          <div className=\"pointer-events-auto flex animate-[rise-in_200ms_var(--ease-hairline)_both] items-center gap-1 rounded-xl border border-border bg-raised p-1 pl-3 text-[0.8125rem] shadow-overlay\">\n            <span className=\"mr-1 font-medium text-fg tabular-nums\">{selCount} selected</span>\n            <span className=\"mx-1 h-4 w-px bg-border\" />\n            {bulkActions?.(selected)}\n            <button\n              type=\"button\"\n              aria-label=\"Clear selection\"\n              onClick={() => setSelected([])}\n              className=\"grid size-7 place-items-center rounded-md text-fg-subtle outline-none hover:bg-sunken hover:text-fg focus-visible:ring-2 focus-visible:ring-accent\"\n            >\n              <XIcon className=\"size-3.5\" />\n            </button>\n          </div>\n        </div>\n      ) : null}\n    </div>\n  )\n}\n\nexport { DataTable }\nexport type { DataTableProps, Sort as DataTableSort }\n"
    }
  ]
}
