{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table",
  "title": "Table",
  "description": "Sortable columns, row selection with Shift-click ranges, and bulk actions.",
  "registryDependencies": [
    "https://creance.pages.dev/r/creance-base.json"
  ],
  "files": [
    {
      "path": "table/Table.css",
      "content": ".cr-table {\n  display: grid;\n  gap: var(--cr-space-3);\n  color: var(--cr-color-text-default);\n  font-family: var(--cr-font-family-sans);\n  font-size: var(--cr-font-size-md);\n}\n\n.cr-table__scroll {\n  overflow-x: auto;\n  border-radius: var(--cr-radius-lg);\n}\n\n.cr-table__table {\n  width: 100%;\n  border-collapse: collapse;\n}\n\n.cr-table__caption {\n  padding: var(--cr-space-3) 14px 0;\n  font-weight: var(--cr-font-weight-semibold);\n  text-align: start;\n}\n\n.cr-table__table th,\n.cr-table__table td {\n  padding: var(--cr-space-3) 14px;\n  border-bottom: 1px solid var(--cr-color-border-default);\n  text-align: start;\n}\n\n.cr-table__table tbody tr:last-child > * {\n  border-bottom: 0;\n}\n\n.cr-table__table thead th {\n  color: var(--cr-color-text-muted);\n  font-size: var(--cr-font-size-xs);\n  font-weight: var(--cr-font-weight-semibold);\n  white-space: nowrap;\n}\n\n.cr-table__table tbody th {\n  font-weight: var(--cr-font-weight-semibold);\n}\n\n.cr-table__table [data-align='end'] {\n  text-align: end;\n  font-variant-numeric: tabular-nums;\n}\n\n.cr-table__table tr[data-selected] > * {\n  background: var(--cr-color-bg-subtle);\n}\n\n.cr-table__check {\n  width: 20px;\n}\n\n.cr-table__check input {\n  width: 18px;\n  height: 18px;\n  margin: 0;\n  accent-color: var(--cr-color-ink-bg);\n}\n\n.cr-table__check input:focus-visible,\n.cr-table__sort:focus-visible {\n  outline: var(--cr-focus-width) solid var(--cr-color-focus);\n  outline-offset: var(--cr-focus-offset);\n}\n\n.cr-table__sort {\n  display: inline-flex;\n  align-items: center;\n  gap: 4px;\n  padding: 0;\n  border: 0;\n  background: none;\n  color: inherit;\n  font: inherit;\n  cursor: pointer;\n}\n\n.cr-table__sort .cr-icon {\n  opacity: 0;\n  transition: transform var(--cr-motion-duration-fast) var(--cr-motion-easing-standard);\n}\n\n.cr-table__sort:hover .cr-icon {\n  opacity: 0.5;\n}\n\n.cr-table__sort[data-active] {\n  color: var(--cr-color-text-default);\n}\n\n.cr-table__sort[data-active] .cr-icon {\n  opacity: 1;\n}\n\n.cr-table__sort .cr-icon[data-direction='ascending'] {\n  transform: rotate(180deg);\n}\n\n.cr-table__empty {\n  padding: var(--cr-space-8) 14px;\n  color: var(--cr-color-text-muted);\n  text-align: center !important;\n}\n\n.cr-table__bulk {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n  gap: var(--cr-space-3);\n  justify-self: end;\n  padding: 6px 6px 6px var(--cr-space-4);\n  border-radius: var(--cr-radius-full);\n  background: var(--cr-color-ink-bg);\n  color: var(--cr-color-ink-fg);\n  font-size: var(--cr-font-size-sm);\n  font-weight: var(--cr-font-weight-semibold);\n}\n\n.cr-table__bulk .cr-button {\n  min-height: 30px;\n  border-color: transparent;\n  background: transparent;\n  box-shadow: none;\n  color: inherit;\n}\n\n.cr-table__bulk .cr-button:hover:not(:disabled) {\n  background: var(--cr-color-ink-bg-hover);\n}\n",
      "type": "registry:file",
      "target": "@components/creance/table/Table.css"
    },
    {
      "path": "table/Table.tsx",
      "content": "'use client';\n\nimport { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport { cx } from '@/lib/creance/cx';\nimport { ArrowDownIcon } from '@/lib/creance/icons';\nimport './Table.css';\n\nexport type SortDirection = 'ascending' | 'descending';\n\nexport interface TableColumn<T> {\n  key: string;\n  header: ReactNode;\n  cell: (row: T) => ReactNode;\n  /** Makes the column sortable by this value. */\n  sortValue?: (row: T) => string | number;\n  align?: 'start' | 'end';\n  /** The column that names each row; its cell becomes a row header for screen readers. */\n  rowHeader?: boolean;\n  width?: string;\n}\n\nexport interface TableProps<T> {\n  columns: TableColumn<T>[];\n  rows: T[];\n  rowKey: (row: T) => string;\n  /** Describes the table. Visually hidden unless `showCaption`. */\n  caption: ReactNode;\n  showCaption?: boolean;\n  /** Starting sort, or a controlled one with `onSortChange`. */\n  sort?: { key: string; direction: SortDirection } | null;\n  defaultSort?: { key: string; direction: SortDirection } | null;\n  onSortChange?: (sort: { key: string; direction: SortDirection }) => void;\n  /** Adds a checkbox column. Shift-click selects a range. */\n  selectable?: boolean;\n  selected?: string[];\n  onSelectedChange?: (keys: string[]) => void;\n  /** Shown in a bar above the table while rows are selected. */\n  bulkActions?: (keys: string[]) => ReactNode;\n  /** How a row is named in its checkbox label: \"Select Run 41\". Defaults to the row key. */\n  rowLabel?: (row: T) => string;\n  emptyMessage?: ReactNode;\n  className?: string;\n}\n\n/** A real table: sortable headers announce their order, rows can be selected for bulk actions. */\nexport function Table<T>({\n  columns,\n  rows,\n  rowKey,\n  caption,\n  showCaption,\n  sort,\n  defaultSort = null,\n  onSortChange,\n  selectable,\n  selected,\n  onSelectedChange,\n  bulkActions,\n  rowLabel,\n  emptyMessage = 'Nothing here yet',\n  className,\n}: TableProps<T>) {\n  const [ownSort, setOwnSort] = useState(defaultSort);\n  const activeSort = sort === undefined ? ownSort : sort;\n  const [ownSelected, setOwnSelected] = useState<string[]>([]);\n  const picked = selected ?? ownSelected;\n  const lastClicked = useRef<number | null>(null);\n  const all = useRef<HTMLInputElement>(null);\n\n  const sorted = useMemo(() => {\n    const col = columns.find((c) => c.key === activeSort?.key);\n    if (!col?.sortValue || !activeSort) return rows;\n    const dir = activeSort.direction === 'ascending' ? 1 : -1;\n    return [...rows].sort((a, b) => {\n      const x = col.sortValue!(a);\n      const y = col.sortValue!(b);\n      return (x < y ? -1 : x > y ? 1 : 0) * dir;\n    });\n  }, [rows, columns, activeSort]);\n\n  const keys = sorted.map(rowKey);\n  const allPicked = keys.length > 0 && keys.every((k) => picked.includes(k));\n  const somePicked = !allPicked && keys.some((k) => picked.includes(k));\n  useEffect(() => {\n    if (all.current) all.current.indeterminate = somePicked;\n  }, [somePicked]);\n\n  const setPicked = (next: string[]) => {\n    if (selected === undefined) setOwnSelected(next);\n    onSelectedChange?.(next);\n  };\n\n  const toggleSort = (key: string) => {\n    const next = {\n      key,\n      direction: (activeSort?.key === key && activeSort.direction === 'ascending' ? 'descending' : 'ascending') as SortDirection,\n    };\n    if (sort === undefined) setOwnSort(next);\n    onSortChange?.(next);\n  };\n\n  const toggleRow = (index: number, shift: boolean) => {\n    const key = keys[index]!;\n    const on = !picked.includes(key);\n    let range = [key];\n    if (shift && lastClicked.current !== null) {\n      const [a, b] = [lastClicked.current, index].sort((m, n) => m - n);\n      range = keys.slice(a, b! + 1);\n    }\n    lastClicked.current = index;\n    setPicked(on ? [...new Set([...picked, ...range])] : picked.filter((k) => !range.includes(k)));\n  };\n\n  return (\n    <div className={cx('cr-table', className)}>\n      {selectable && bulkActions && picked.length > 0 && (\n        <div className=\"cr-table__bulk\" role=\"region\" aria-label=\"Bulk actions\">\n          <span>{picked.length} selected</span>\n          {bulkActions(picked)}\n        </div>\n      )}\n      <div className=\"cr-table__scroll cr-surface\">\n        <table className=\"cr-table__table\">\n          <caption className={showCaption ? 'cr-table__caption' : 'cr-visually-hidden'}>{caption}</caption>\n          <thead>\n            <tr>\n              {selectable && (\n                <th scope=\"col\" className=\"cr-table__check\">\n                  <input\n                    ref={all}\n                    type=\"checkbox\"\n                    aria-label=\"Select all rows\"\n                    checked={allPicked}\n                    onChange={() => setPicked(allPicked ? picked.filter((k) => !keys.includes(k)) : [...new Set([...picked, ...keys])])}\n                  />\n                </th>\n              )}\n              {columns.map((c) => {\n                const dir = activeSort?.key === c.key ? activeSort.direction : undefined;\n                return (\n                  <th key={c.key} scope=\"col\" aria-sort={c.sortValue ? (dir ?? 'none') : undefined} data-align={c.align} style={{ width: c.width }}>\n                    {c.sortValue ? (\n                      <button type=\"button\" className=\"cr-table__sort\" data-active={dir ? '' : undefined} onClick={() => toggleSort(c.key)}>\n                        {c.header}\n                        <ArrowDownIcon data-direction={dir} />\n                      </button>\n                    ) : (\n                      c.header\n                    )}\n                  </th>\n                );\n              })}\n            </tr>\n          </thead>\n          <tbody>\n            {sorted.length === 0 && (\n              <tr>\n                <td className=\"cr-table__empty\" colSpan={columns.length + (selectable ? 1 : 0)}>\n                  {emptyMessage}\n                </td>\n              </tr>\n            )}\n            {sorted.map((row, i) => {\n              const key = keys[i]!;\n              const on = picked.includes(key);\n              return (\n                <tr key={key} data-selected={on || undefined}>\n                  {selectable && (\n                    <td className=\"cr-table__check\">\n                      <input\n                        type=\"checkbox\"\n                        aria-label={`Select ${rowLabel ? rowLabel(row) : key}`}\n                        checked={on}\n                        onChange={() => {}}\n                        onClick={(e) => toggleRow(i, e.shiftKey)}\n                      />\n                    </td>\n                  )}\n                  {columns.map((c) => {\n                    const Cell = c.rowHeader ? 'th' : 'td';\n                    return (\n                      <Cell key={c.key} scope={c.rowHeader ? 'row' : undefined} data-align={c.align}>\n                        {c.cell(row)}\n                      </Cell>\n                    );\n                  })}\n                </tr>\n              );\n            })}\n          </tbody>\n        </table>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/creance/table/Table.tsx"
    }
  ],
  "categories": [
    "molecule",
    "data-display"
  ],
  "type": "registry:ui"
}