{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "title": "Data table",
  "description": "Records you narrow with a sentence, sort by pressing a heading (the column turns ink and the rows glide), and page at a folio, or read on as the table ends in the rest of itself.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json",
    "https://thedirectors.agency/ui/r/button.json",
    "https://thedirectors.agency/ui/r/pagination.json",
    "https://thedirectors.agency/ui/r/select.json",
    "https://thedirectors.agency/ui/r/table.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/data-table.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from \"@/components/ui/pagination\"\nimport { Select } from \"@/components/ui/select\"\nimport { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, type TableProps } from \"@/components/ui/table\"\n\ntype DataTableColumn<T> = {\n  id: string\n  header: string\n  /** What the column sorts by, and what it shows unless `cell` says otherwise. */\n  value: (row: T) => string | number\n  cell?: (row: T) => React.ReactNode\n  /** Right-aligned figures. Its first sort is largest first. */\n  numeric?: boolean\n  /** The row's name. */\n  primary?: boolean\n  /** Set false for a column you can't sort by. */\n  sortable?: boolean\n}\n\ntype DataTableFilter<T> = { id: string; label: string; test: (row: T) => boolean }\n\ntype Sort = { id: string; dir: 1 | -1 }\n\ntype DataTableProps<T> = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  data: T[]\n  columns: DataTableColumn<T>[]\n  /** What the rows are, in the plural: \"projects\". It finishes the filter's sentence and the count. */\n  noun?: string\n  /** Names the table for screen readers. */\n  caption: string\n  /** A stable name for a row, so it keeps its place (and glides) through sorting and filtering. Defaults to its index in data. */\n  getRowId?: (row: T, index: number) => string\n  /** The ways to narrow the rows. The sentence \"Show [all] projects\" offers them; \"all\" is always first. */\n  filters?: DataTableFilter<T>[]\n  filter?: string\n  defaultFilter?: string\n  onFilterChange?: (filter: string) => void\n  defaultSort?: Sort\n  page?: number\n  defaultPage?: number\n  onPageChange?: (page: number) => void\n  pageSize?: number\n  /** Where a page's link goes when opened in a new tab. A plain press turns the page in place. */\n  pageHref?: (page: number) => string\n  /** folio: a page at a time, the book's folio at the foot. tail: the table ends in the rest of itself (\"and 6 more\"). */\n  variant?: \"folio\" | \"tail\"\n  /** The table's own setting: ink, or forte (the sorted column set loud). */\n  tableVariant?: Exclude<TableProps[\"variant\"], \"cross\">\n}\n\nconst byIndex = (_row: unknown, i: number) => String(i)\nconst pad = (n: number) => String(n).padStart(2, \"0\")\nconst compare = (a: string | number, b: string | number) => (typeof a === \"number\" && typeof b === \"number\" ? a - b : String(a).localeCompare(String(b)))\n\n/**\n * Records you can narrow, sort and page. The filter is a sentence (\"Show identity projects\"); pressing a heading sorts by\n * it, the column turns ink and the rows glide to their places; the pages turn at a folio, or the table ends in the rest\n * of itself. Rows that come into view (a new page, the rest shown) arrive in turn.\n */\nfunction DataTable<T>({\n  data,\n  columns,\n  noun = \"rows\",\n  caption,\n  getRowId = byIndex,\n  filters = [],\n  filter,\n  defaultFilter = \"all\",\n  onFilterChange,\n  defaultSort,\n  page,\n  defaultPage = 1,\n  onPageChange,\n  pageSize = 8,\n  pageHref = (n) => `?page=${n}`,\n  variant = \"folio\",\n  tableVariant,\n  className,\n  ...props\n}: DataTableProps<T>) {\n  const [ownFilter, setOwnFilter] = React.useState(defaultFilter)\n  const [ownPage, setOwnPage] = React.useState(defaultPage)\n  const [sort, setSort] = React.useState<Sort | null>(defaultSort ?? null)\n  const [arriving, setArriving] = React.useState<number | null>(null) // rows from this index arrive in turn\n\n  const chosen = filters.find((f) => f.id === (filter ?? ownFilter))\n  const rows = React.useMemo(() => {\n    const all = data.map((row, i) => ({ row, id: getRowId(row, i) }))\n    const kept = chosen ? all.filter(({ row }) => chosen.test(row)) : all\n    const col = columns.find((c) => c.id === sort?.id)\n    return col && sort ? [...kept].sort((a, b) => compare(col.value(a.row), col.value(b.row)) * sort.dir) : kept\n  }, [data, getRowId, chosen, columns, sort])\n\n  const size = Math.max(1, Math.floor(pageSize))\n  const pages = Math.max(1, Math.ceil(rows.length / size))\n  const current = Math.min(pages, Math.max(1, page ?? ownPage))\n  const from = variant === \"folio\" ? (current - 1) * size : 0\n  const to = Math.min(rows.length, current * size)\n  const shown = rows.slice(from, to)\n\n  function turn(next: number, arriveFrom: number | null) {\n    setArriving(arriveFrom)\n    if (page === undefined) setOwnPage(next)\n    onPageChange?.(next)\n  }\n  function narrow(next: string) {\n    setArriving(null)\n    if (filter === undefined) setOwnFilter(next)\n    onFilterChange?.(next)\n    if (page === undefined) setOwnPage(1)\n    onPageChange?.(1)\n  }\n  const go = (n: number) => ({\n    href: pageHref(n),\n    onClick(e: React.MouseEvent<HTMLAnchorElement>) {\n      if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return // a new tab: let the link be a link\n      e.preventDefault()\n      if (n !== current) turn(n, 0)\n      // A step that runs out of pages stops being a link, which drops focus. Keep it in the pager, on the page.\n      const nav = e.currentTarget.closest(\"nav\")\n      requestAnimationFrame(() => {\n        if (nav && !nav.contains(document.activeElement)) (nav.querySelector<HTMLElement>('[aria-current=\"page\"]') ?? nav.querySelector<HTMLElement>(\"a[href]\"))?.focus()\n      })\n    },\n  })\n\n  const kind = chosen ? `${chosen.label} ` : \"\"\n  const left = rows.length - to\n\n  return (\n    <div data-slot=\"data-table\" data-variant={variant} className={cn(\"ot-data-table\", className)} {...props}>\n      {filters.length ? (\n        <p className=\"ot-data-table-filter\">\n          <Select label=\"Show\" value={chosen ? chosen.id : \"all\"} onChange={(e) => narrow(e.target.value)}>\n            <option value=\"all\">all</option>\n            {filters.map((f) => (\n              <option key={f.id} value={f.id}>\n                {f.label}\n              </option>\n            ))}\n          </Select>{\" \"}\n          <span>{noun}</span>\n        </p>\n      ) : null}\n      <Table variant={tableVariant}>\n        <TableCaption>{caption}</TableCaption>\n        <TableHeader>\n          <TableRow>\n            {columns.map((c) => (\n              <TableHead key={c.id} numeric={c.numeric} sort={sort?.id === c.id ? (sort.dir === 1 ? \"ascending\" : \"descending\") : undefined}>\n                {c.sortable === false ? (\n                  c.header\n                ) : (\n                  <button\n                    type=\"button\"\n                    onClick={() => {\n                      setArriving(null)\n                      setSort({ id: c.id, dir: sort?.id === c.id ? (-sort.dir as 1 | -1) : c.numeric ? -1 : 1 })\n                    }}\n                  >\n                    {c.header}\n                  </button>\n                )}\n              </TableHead>\n            ))}\n          </TableRow>\n        </TableHeader>\n        <TableBody>\n          {shown.map(({ row, id }, i) => (\n            <TableRow\n              key={id}\n              data-arriving={arriving !== null && from + i >= arriving ? \"\" : undefined}\n              style={arriving !== null ? ({ \"--k\": from + i - arriving } as React.CSSProperties) : undefined}\n            >\n              {columns.map((c) => (\n                <TableCell key={c.id} numeric={c.numeric} primary={c.primary} sorted={sort?.id === c.id}>\n                  {c.cell ? c.cell(row) : c.value(row)}\n                </TableCell>\n              ))}\n            </TableRow>\n          ))}\n          {shown.length ? null : (\n            <TableRow>\n              <TableCell colSpan={columns.length} className=\"ot-data-table-empty\">\n                {data.length ? `No ${kind}${noun}.` : `No ${noun} yet.`}{\" \"}\n                {chosen ? (\n                  <Button variant=\"quiet\" onClick={() => narrow(\"all\")}>\n                    Show all {noun}\n                  </Button>\n                ) : null}\n              </TableCell>\n            </TableRow>\n          )}\n        </TableBody>\n      </Table>\n      <div className=\"ot-data-table-foot\">\n        <p className=\"ot-data-table-count\" role=\"status\">\n          {from === 0 && to === rows.length ? `${rows.length} ${noun}` : variant === \"tail\" ? `${to} of ${rows.length} ${noun}` : `${from + 1} to ${to} of ${rows.length} ${noun}`}\n        </p>\n        {variant === \"folio\" && pages > 1 ? (\n          <Pagination aria-label={`Pages of ${noun}`}>\n            <PaginationContent variant=\"folio\">\n              <PaginationItem><PaginationPrevious {...(current > 1 ? go(current - 1) : {})} /></PaginationItem>\n              <PaginationItem><PaginationLink {...go(current)} aria-label={`Page ${current} of ${pages}`} isActive>{pad(current)}</PaginationLink></PaginationItem>\n              <PaginationItem aria-hidden=\"true\">{pad(pages)}</PaginationItem>\n              <PaginationItem><PaginationNext {...(current < pages ? go(current + 1) : {})} /></PaginationItem>\n            </PaginationContent>\n          </Pagination>\n        ) : null}\n        {variant === \"tail\" && rows.length > size ? (\n          <Button\n            variant=\"quiet\"\n            className=\"ot-data-table-more\"\n            aria-label={left > size ? `and ${left} more, show the next ${size}` : undefined}\n            onClick={() => (left > 0 ? turn(current + 1, to) : turn(1, null))}\n          >\n            {left > 0 ? `and ${left} more` : \"Show fewer\"}\n          </Button>\n        ) : null}\n      </div>\n    </div>\n  )\n}\n\nexport { DataTable, type DataTableProps, type DataTableColumn, type DataTableFilter }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/data-table.css",
      "content": "@layer components {\n  /* ── ot-data-table ── records you can narrow, sort and page. The filter is a sentence over the table; the count and the folio\n     stand on the foot line, the count at the start in small print, the folio at the end. Rows that come into view arrive in turn. */\n  .ot-data-table { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--ot-space-5); min-width: 0; }\n  .ot-data-table-filter { font-size: var(--ot-mp); line-height: var(--ot-mp-lh); color: var(--ot-graphite); }\n  .ot-data-table-foot { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--ot-space-3) var(--ot-space-5); margin-block-start: calc(-1 * var(--ot-space-2)); }\n  .ot-data-table-count { font-size: var(--ot-pp); line-height: var(--ot-pp-lh); letter-spacing: var(--ot-pp-tr); color: var(--ot-pencil); font-variant-numeric: tabular-nums; }\n  .ot-data-table-empty { padding-block: var(--ot-space-6); color: var(--ot-graphite); }\n  .ot-data-table tr[data-arriving] { animation: ot-arrive var(--ot-moderato) var(--ot-exhale) backwards; animation-delay: calc(var(--k, 0) * 2 * var(--ot-arpeggio)); }\n\n  /* ── tail ── the table ends in the rest of itself, as the collapsible does: \"and 16 more\" is the control, set as the\n     table's caption straight under the last rule; the next rows arrive in turn under it. */\n  .ot-data-table[data-variant=\"tail\"] .ot-data-table-more { order: -1; }\n  .ot-data-table-more { font-size: var(--ot-pp); letter-spacing: var(--ot-pp-tr); margin-inline-start: -0.1em; }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/data-table.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/data-table.css\"": {}
  },
  "meta": {
    "movement": "X",
    "underneath": "native",
    "contract": "ot-data-table"
  },
  "type": "registry:ui"
}
