{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table",
  "type": "registry:ui",
  "title": "Table",
  "description": "Rows of things, read across and compared down: engraved labels and hairlines; sorting turns the column's arrow and every row travels to its new place on the settle spring; rows select with the checkbox.",
  "dependencies": [],
  "registryDependencies": [
    "https://metalui.dev/r/tokens.json"
  ],
  "files": [
    {
      "path": "packages/metalui/src/components/table/table.tsx",
      "type": "registry:ui",
      "target": "components/metalui/table/table.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { Checkbox } from '../checkbox/checkbox';\n\n/* ─────────────────────────────────────────────────────────\n * TABLE, rows of a person's things, read across and compared down\n *\n *   rest      engraved column labels over rows parted by engraved hairlines\n *   hover     a row sinks a touch (the switcher's track look)\n *   sort      a sortable label is a button; its arrow turns on the part spring, and every row\n *             travels from where it was to where it now belongs on the settle spring\n *   select    the row checkbox; select-all in the head is mixed when some are chosen; chosen rows\n *             carry a quiet green tint\n *   empty     one quiet line says there is nothing\n * Reduce Motion: rows jump to their places; the arrow turns at once.\n * An object: it stands for a person's things. It uses the label, the rule and the checkbox.\n * ───────────────────────────────────────────────────────── */\n\nexport interface TableColumn<Row> {\n  key: string;\n  header: string;\n  /** What the cell shows (the row's field by default). */\n  cell?: (row: Row) => React.ReactNode;\n  /** Makes the column sortable by this value. */\n  sortBy?: (row: Row) => string | number;\n  align?: 'start' | 'end';\n}\n\nexport type SortState = { key: string; direction: 'ascending' | 'descending' } | null;\n\nexport interface TableProps<Row> {\n  columns: TableColumn<Row>[];\n  rows: Row[];\n  rowKey: (row: Row) => string;\n  /** Names the table: a caption above it (visible) or only for assistive tech (`captionHidden`). */\n  caption: string;\n  captionHidden?: boolean;\n  sort?: SortState;\n  defaultSort?: SortState;\n  onSortChange?: (sort: SortState) => void;\n  /** Turns on the checkbox column. */\n  selected?: Set<string>;\n  /** Names a row's checkbox (\"Select Lisbon\"); the first column's value by default. */\n  rowLabel?: (row: Row) => string;\n  onSelectedChange?: (selected: Set<string>) => void;\n  /** What to say when there are no rows. */\n  empty?: React.ReactNode;\n  className?: string;\n}\n\nconst TABLE = 'mu-table w-full table-reset';\nconst CAPTION = 'mu-table-caption caption-top text-left pb-table-caption-gap type-title text-ink';\nconst TH = 'mu-table-th align-middle h-table-head-height px-table-row-pad-x type-label engraved text-left font-normal table-rule data-end:text-right';\nconst SORT = 'mu-table-sort inline-flex items-center gap-4 border-0 bg-transparent p-0 table-sort-button cursor-pointer outline-none focus-visible:focus-ring';\nconst ARROW = 'mu-table-arrow size-table-sort-glyph table-sort-arrow reduced-motion:transition-none';\nconst TR = 'mu-table-row transition-row hover:not-data-selected:recipe-switcher data-selected:bg-table-select-tint';\nconst TD = 'mu-table-td align-middle h-table-row-height px-table-row-pad-x type-ui text-ink table-rule data-end:text-right data-end:tabular-nums';\nconst EMPTY = 'mu-table-empty h-table-row-height px-table-row-pad-x type-body text-ink3 text-center';\nconst CHECK = 'mu-table-check w-table-row-height px-table-row-pad-x';\n\nfunction readMotion(el: Element) {\n  const s = getComputedStyle(el);\n  const ms = parseFloat(s.getPropertyValue('--mu-spring-settle-d')) * 1000 * (parseFloat(s.getPropertyValue('--mu-travel-settle')) || 0);\n  return { ms, easing: s.getPropertyValue('--mu-spring-settle').trim() || 'ease-out' };\n}\n\n/** Rows of things, sortable and selectable. */\nexport function Table<Row>({ columns, rows, rowKey, caption, captionHidden, sort, defaultSort = null, onSortChange, selected, onSelectedChange, rowLabel, empty = 'Nothing here yet.', className }: TableProps<Row>) {\n  const [ownSort, setOwnSort] = React.useState<SortState>(defaultSort);\n  const current = sort !== undefined ? sort : ownSort;\n  const body = React.useRef<HTMLTableSectionElement>(null);\n  const tops = React.useRef(new Map<string, number>());\n\n  const sorted = React.useMemo(() => {\n    const col = current && columns.find((c) => c.key === current.key);\n    if (!col?.sortBy) return rows;\n    const by = col.sortBy;\n    const dir = current!.direction === 'ascending' ? 1 : -1;\n    return [...rows].sort((a, b) => {\n      const x = by(a), y = by(b);\n      return (typeof x === 'number' && typeof y === 'number' ? x - y : String(x).localeCompare(String(y))) * dir;\n    });\n  }, [rows, columns, current]);\n\n  // Each row travels from where it was to where it now is (FLIP), measured inside the table body.\n  React.useLayoutEffect(() => {\n    const tb = body.current;\n    if (!tb) return;\n    const next = new Map<string, number>();\n    const { ms, easing } = readMotion(tb);\n    tb.querySelectorAll<HTMLTableRowElement>('tr[data-key]').forEach((tr) => {\n      const key = tr.dataset.key!;\n      const top = tr.offsetTop;\n      const was = tops.current.get(key);\n      next.set(key, top);\n      if (was != null && was !== top && ms > 0) tr.animate([{ transform: `translateY(${was - top}px)` }, { transform: 'none' }], { duration: ms, easing });\n    });\n    tops.current = next;\n  }, [sorted]);\n\n  const toggleSort = (key: string) => {\n    const next: SortState = current?.key === key ? { key, direction: current.direction === 'ascending' ? 'descending' : 'ascending' } : { key, direction: 'ascending' };\n    if (sort === undefined) setOwnSort(next);\n    onSortChange?.(next);\n  };\n\n  const selectable = selected != null;\n  const all = rows.length > 0 && selectable && rows.every((r) => selected!.has(rowKey(r)));\n  const some = selectable && !all && rows.some((r) => selected!.has(rowKey(r)));\n  const setAll = (on: boolean) => onSelectedChange?.(on ? new Set(rows.map(rowKey)) : new Set());\n  const setOne = (key: string, on: boolean) => {\n    const next = new Set(selected);\n    if (on) next.add(key); else next.delete(key);\n    onSelectedChange?.(next);\n  };\n\n  return (\n    <table className={className ? `${TABLE} ${className}` : TABLE}>\n      <caption className={captionHidden ? 'sr-only' : CAPTION}>{caption}</caption>\n      <thead>\n        <tr>\n          {selectable && (\n            <th scope=\"col\" className={`${TH} ${CHECK}`}>\n              <Checkbox size=\"row\" aria-label=\"Select all\" checked={all} doing={some} onCheckedChange={(on) => setAll(!!on)} />\n            </th>\n          )}\n          {columns.map((c) => {\n            const sorted = current?.key === c.key ? current.direction : undefined;\n            return (\n              <th key={c.key} scope=\"col\" aria-sort={c.sortBy ? sorted ?? 'none' : undefined} data-end={c.align === 'end' ? '' : undefined} className={TH}>\n                {c.sortBy ? (\n                  <button type=\"button\" className={SORT} onClick={() => toggleSort(c.key)}>\n                    {c.header}\n                    <svg aria-hidden viewBox=\"0 0 10 10\" className={ARROW} fill=\"none\" stroke=\"currentColor\" strokeWidth={1.4} strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M5 8V2M2.5 4.5 5 2l2.5 2.5\" /></svg>\n                  </button>\n                ) : c.header}\n              </th>\n            );\n          })}\n        </tr>\n      </thead>\n      <tbody ref={body}>\n        {sorted.length === 0 ? (\n          <tr><td colSpan={columns.length + (selectable ? 1 : 0)} className={EMPTY}>{empty}</td></tr>\n        ) : sorted.map((r) => {\n          const key = rowKey(r);\n          const on = selectable && selected!.has(key);\n          return (\n            <tr key={key} data-key={key} data-selected={on ? '' : undefined} aria-selected={selectable ? on : undefined} className={TR}>\n              {selectable && (\n                <td className={`${TD} ${CHECK}`}>\n                  <Checkbox size=\"row\" aria-label={`Select ${rowLabel ? rowLabel(r) : String((r as Record<string, unknown>)[columns[0].key] ?? key)}`} checked={on} onCheckedChange={(v) => setOne(key, !!v)} />\n                </td>\n              )}\n              {columns.map((c) => (\n                <td key={c.key} data-end={c.align === 'end' ? '' : undefined} className={TD}>\n                  {c.cell ? c.cell(r) : String((r as Record<string, unknown>)[c.key] ?? '')}\n                </td>\n              ))}\n            </tr>\n          );\n        })}\n      </tbody>\n    </table>\n  );\n}\n"
    }
  ],
  "docs": "Agent guide: https://metalui.dev/r/table.md. SwiftUI: MetalTable in the MetalUI Swift package."
}
