{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "tablekit contributors",
  "docs": "Docs for agents: https://tablekit.amitpatjoshi.com/llms.txt — rules: components/tablekit/AGENTS.md",
  "categories": [
    "table",
    "data-table",
    "data"
  ],
  "name": "tablekit",
  "type": "registry:block",
  "title": "tablekit data table",
  "description": "Accessible, token-driven data table: sorting, filtering, pagination, selection, column resize/visibility, responsive cards, and a declarative JSON schema for agents. Themed via your shadcn CSS variables.",
  "dependencies": [
    "zod@^4.1.0",
    "lucide-react@^1.48.0"
  ],
  "files": [
    {
      "path": "registry/tablekit/core/badge-icons.ts",
      "type": "registry:lib",
      "target": "components/tablekit/core/badge-icons.ts",
      "content": "// Zod-free, so the main @tablekit/core entry can export these without pulling in zod.\nimport type { TONES } from \"./schema\";\n\n/**\n * Badge icons: a closed set of Lucide icon names, so generated schemas can't reference icons\n * that don't exist. The React package maps each name to its Lucide component.\n */\nexport const BADGE_ICONS = [\n  \"circle-check\",\n  \"check\",\n  \"circle-x\",\n  \"x\",\n  \"triangle-alert\",\n  \"circle-alert\",\n  \"info\",\n  \"circle-dashed\",\n  \"circle\",\n  \"circle-dot\",\n  \"circle-pause\",\n  \"clock\",\n  \"hourglass\",\n  \"loader\",\n  \"refresh-cw\",\n  \"ban\",\n  \"lock\",\n  \"shield-check\",\n  \"arrow-up\",\n  \"arrow-down\",\n  \"arrow-right\",\n  \"undo-2\",\n  \"send\",\n  \"truck\",\n  \"star\",\n  \"zap\",\n  \"sparkles\",\n  \"eye\",\n] as const;\n\n/** Icon each tone uses when `badge.indicator` is \"icon\" and no per-value icon is given. */\nexport const TONE_ICONS: Record<(typeof TONES)[number], BadgeIconName> = {\n  neutral: \"circle-dashed\",\n  info: \"info\",\n  success: \"circle-check\",\n  warning: \"triangle-alert\",\n  danger: \"circle-x\",\n  accent: \"sparkles\",\n};\n\nexport type BadgeIconName = (typeof BADGE_ICONS)[number];\n\n/** Icons for row / bulk actions: a closed set of Lucide names, mapped in @tablekit/react. */\nexport const ACTION_ICONS = [\n  \"eye\",\n  \"pencil\",\n  \"copy\",\n  \"download\",\n  \"upload\",\n  \"share-2\",\n  \"send\",\n  \"external-link\",\n  \"refresh-cw\",\n  \"undo-2\",\n  \"archive\",\n  \"trash-2\",\n  \"ban\",\n  \"lock\",\n  \"unlock\",\n  \"check\",\n  \"x\",\n  \"user-plus\",\n  \"mail\",\n  \"receipt\",\n  \"flag\",\n  \"star\",\n] as const;\n\nexport type ActionIconName = (typeof ACTION_ICONS)[number];\n\n/** How an actions column presents its actions. */\n/** Multiple row actions go in a ⋯ menu or as icon buttons, never as a row of text buttons. */\nexport const ACTIONS_DISPLAYS = [\"menu\", \"inline\"] as const;\nexport type ActionsDisplay = (typeof ACTIONS_DISPLAYS)[number];\n"
    },
    {
      "path": "registry/tablekit/core/columns.ts",
      "type": "registry:lib",
      "target": "components/tablekit/core/columns.ts",
      "content": "import type { ColumnDef, RowData } from \"./types\";\nimport { clamp } from \"./utils\";\n\nexport const DEFAULT_MIN_WIDTH = 64;\nexport const DEFAULT_MAX_WIDTH = 800;\n\nexport function isColumnVisible(visibility: Record<string, boolean>, columnId: string): boolean {\n  return visibility[columnId] !== false;\n}\n\nexport function isColumnPinned<T extends RowData>(\n  column: ColumnDef<T>,\n  pinning: Record<string, boolean> = {},\n): boolean {\n  return pinning[column.id] ?? column.pinned === true;\n}\n\n/**\n * Every column in display order, with `pinned` resolved from state: pinned columns first,\n * then the rest, each group in `order` (ids missing from `order` keep declaration order at the end).\n */\nexport function getOrderedColumns<T extends RowData>(\n  columns: readonly ColumnDef<T>[],\n  order: readonly string[] = [],\n  pinning: Record<string, boolean> = {},\n): ColumnDef<T>[] {\n  const rank = new Map(order.map((id, i) => [id, i]));\n  const sorted = columns\n    .map((c, i) => ({ c, r: rank.get(c.id) ?? order.length + i }))\n    .sort((a, b) => a.r - b.r)\n    .map(({ c }) => {\n      const pinned = isColumnPinned(c, pinning);\n      return pinned === (c.pinned === true) ? c : { ...c, pinned };\n    });\n  return [...sorted.filter((c) => c.pinned), ...sorted.filter((c) => !c.pinned)];\n}\n\nexport function getVisibleColumns<T extends RowData>(\n  columns: readonly ColumnDef<T>[],\n  visibility: Record<string, boolean>,\n  order?: readonly string[],\n  pinning?: Record<string, boolean>,\n): ColumnDef<T>[] {\n  return getOrderedColumns(columns, order, pinning).filter((c) =>\n    isColumnVisible(visibility, c.id),\n  );\n}\n\n/** Full column order as ids (resolves an empty or partial `order`). */\nexport function resolveColumnOrder<T extends RowData>(\n  columns: readonly ColumnDef<T>[],\n  order: readonly string[] = [],\n  pinning: Record<string, boolean> = {},\n): string[] {\n  return getOrderedColumns(columns, order, pinning).map((c) => c.id);\n}\n\n/**\n * Move `columnId` to where `targetId` is now. Both must be in the same group (pinned or not);\n * otherwise the order is returned unchanged. Returns a full id list.\n */\nexport function moveColumn<T extends RowData>(\n  columns: readonly ColumnDef<T>[],\n  order: readonly string[],\n  pinning: Record<string, boolean>,\n  columnId: string,\n  targetId: string,\n): string[] {\n  const ordered = getOrderedColumns(columns, order, pinning);\n  const ids = ordered.map((c) => c.id);\n  const from = ids.indexOf(columnId);\n  const to = ids.indexOf(targetId);\n  if (from < 0 || to < 0 || from === to) return ids;\n  if (ordered[from]?.pinned !== ordered[to]?.pinned) return ids;\n  ids.splice(from, 1);\n  ids.splice(to, 0, columnId);\n  return ids;\n}\n\n/**\n * Pin or unpin a column. It lands at the boundary between the groups: the end of the\n * pinned group when pinned, the start of the unpinned group when unpinned.\n */\nexport function setColumnPinned<T extends RowData>(\n  columns: readonly ColumnDef<T>[],\n  order: readonly string[],\n  pinning: Record<string, boolean>,\n  columnId: string,\n  pinned: boolean,\n): { columnOrder: string[]; columnPinning: Record<string, boolean> } {\n  const nextPinning = { ...pinning, [columnId]: pinned };\n  const ordered = getOrderedColumns(columns, order, pinning).filter((c) => c.id !== columnId);\n  const boundary = ordered.filter((c) => c.pinned).length;\n  const ids = ordered.map((c) => c.id);\n  ids.splice(boundary, 0, columnId);\n  return { columnOrder: ids, columnPinning: nextPinning };\n}\n\nexport function toggleColumnVisibility(\n  visibility: Record<string, boolean>,\n  columnId: string,\n  value?: boolean,\n): Record<string, boolean> {\n  const next = value ?? !isColumnVisible(visibility, columnId);\n  return { ...visibility, [columnId]: next };\n}\n\nexport function getColumnWidth<T extends RowData>(\n  column: ColumnDef<T>,\n  sizing: Record<string, number>,\n): number | undefined {\n  return sizing[column.id] ?? column.width;\n}\n\nexport function resizeColumn<T extends RowData>(\n  sizing: Record<string, number>,\n  column: ColumnDef<T>,\n  width: number,\n): Record<string, number> {\n  const w = clamp(\n    Math.round(width),\n    column.minWidth ?? DEFAULT_MIN_WIDTH,\n    column.maxWidth ?? DEFAULT_MAX_WIDTH,\n  );\n  return { ...sizing, [column.id]: w };\n}\n\n/**\n * Left offsets (px) for pinned columns so each can be `position: sticky; left: <offset>`.\n * `leading` is extra width before the first column (e.g. the selection checkbox).\n */\nexport function getPinnedOffsets<T extends RowData>(\n  columns: readonly ColumnDef<T>[],\n  sizing: Record<string, number>,\n  leading = 0,\n  fallbackWidth = 160,\n): Record<string, number> {\n  const offsets: Record<string, number> = {};\n  let left = leading;\n  for (const c of columns) {\n    if (!c.pinned) break;\n    offsets[c.id] = left;\n    left += getColumnWidth(c, sizing) ?? fallbackWidth;\n  }\n  return offsets;\n}\n\n/** Columns to show at a given priority cutoff. Columns without a priority always show. */\nexport function getColumnsForPriority<T extends RowData>(\n  columns: readonly ColumnDef<T>[],\n  maxPriority: number,\n): ColumnDef<T>[] {\n  return columns.filter((c) => c.pinned || c.priority === undefined || c.priority <= maxPriority);\n}\n\n/** Width a column without an explicit width is assumed to need. */\nexport const DEFAULT_AUTO_WIDTH = 120;\n/** Width assumed for a pinned column without an explicit width. */\nexport const DEFAULT_PINNED_WIDTH = 200;\n\n/** Minimum width (px) the columns need side by side, plus `leading` (e.g. the selection column). */\nexport function getRequiredWidth<T extends RowData>(\n  columns: readonly ColumnDef<T>[],\n  sizing: Record<string, number>,\n  leading = 0,\n): number {\n  return columns.reduce(\n    (sum, c) =>\n      sum + (getColumnWidth(c, sizing) ?? (c.pinned ? DEFAULT_PINNED_WIDTH : DEFAULT_AUTO_WIDTH)),\n    leading,\n  );\n}\n\n/**\n * Responsive \"priority\" mode: keep as many columns as fit in `width`. Drops the least\n * important priority level (5, then 4, …) until the rest fit. Columns without a priority,\n * and pinned columns, always stay; if even they don't fit, the table scrolls.\n */\nexport function fitColumnsToWidth<T extends RowData>(\n  columns: readonly ColumnDef<T>[],\n  sizing: Record<string, number>,\n  width: number,\n  leading = 0,\n): ColumnDef<T>[] {\n  for (let max = 5; max >= 1; max--) {\n    const kept = getColumnsForPriority(columns, max);\n    if (getRequiredWidth(kept, sizing, leading) <= width) return kept;\n  }\n  return getColumnsForPriority(columns, 0);\n}\n"
    },
    {
      "path": "registry/tablekit/core/filtering.ts",
      "type": "registry:lib",
      "target": "components/tablekit/core/filtering.ts",
      "content": "import type { ColumnDef, ColumnFilter, FilterValue, RangeFilterValue, Row, RowData } from \"./types\";\nimport { toComparable } from \"./utils\";\n\nfunction stringify(value: unknown): string {\n  if (value === null || value === undefined) return \"\";\n  if (value instanceof Date) return value.toISOString();\n  if (Array.isArray(value)) return value.map(stringify).join(\" \");\n  if (typeof value === \"object\") return Object.values(value).map(stringify).join(\" \");\n  return String(value);\n}\n\nexport function textFilter(value: unknown, query: string): boolean {\n  const q = query.trim().toLowerCase();\n  if (!q) return true;\n  return stringify(value).toLowerCase().includes(q);\n}\n\nexport function selectFilter(value: unknown, selected: readonly string[]): boolean {\n  if (selected.length === 0) return true;\n  if (Array.isArray(value)) return value.some((v) => selected.includes(String(v)));\n  return selected.includes(String(value));\n}\n\nfunction toRangeNumber(v: unknown): number | null {\n  if (v === null || v === undefined || v === \"\") return null;\n  if (typeof v === \"number\") return v;\n  if (v instanceof Date) return v.getTime();\n  const asNumber = Number(v);\n  if (!Number.isNaN(asNumber)) return asNumber;\n  const asDate = Date.parse(String(v));\n  return Number.isNaN(asDate) ? null : asDate;\n}\n\nexport function rangeFilter(value: unknown, range: RangeFilterValue): boolean {\n  const min = toRangeNumber(range.min);\n  const max = toRangeNumber(range.max);\n  if (min === null && max === null) return true;\n  const n = toRangeNumber(value);\n  if (n === null) return false;\n  if (min !== null && n < min) return false;\n  if (max !== null && n > max) return false;\n  return true;\n}\n\nexport function isFilterActive(value: FilterValue | undefined): boolean {\n  if (value === undefined) return false;\n  if (typeof value === \"string\") return value.trim() !== \"\";\n  if (Array.isArray(value)) return value.length > 0;\n  return toComparable(value.min) !== null || toComparable(value.max) !== null;\n}\n\nfunction matchesColumnFilter<T extends RowData>(\n  row: Row<T>,\n  column: ColumnDef<T>,\n  filterValue: FilterValue,\n): boolean {\n  const value = row.getValue(column.id);\n  if (column.filterFn) return column.filterFn(value, filterValue, row.original);\n  if (typeof filterValue === \"string\") return textFilter(value, filterValue);\n  if (Array.isArray(filterValue)) return selectFilter(value, filterValue);\n  return rangeFilter(value, filterValue);\n}\n\nexport function filterRows<T extends RowData>(\n  rows: Row<T>[],\n  columns: readonly ColumnDef<T>[],\n  globalFilter: string,\n  columnFilters: readonly ColumnFilter[],\n): Row<T>[] {\n  const active = columnFilters.filter((f) => isFilterActive(f.value));\n  const query = globalFilter.trim();\n  if (!query && active.length === 0) return rows;\n\n  const byId = new Map(columns.map((c) => [c.id, c]));\n  const searchable = columns.filter((c) => c.searchable !== false);\n\n  return rows.filter((row) => {\n    for (const f of active) {\n      const column = byId.get(f.id);\n      if (column && !matchesColumnFilter(row, column, f.value)) return false;\n    }\n    if (query) return searchable.some((c) => textFilter(row.getValue(c.id), query));\n    return true;\n  });\n}\n\n/** Set (or clear, when inactive) the filter for one column. */\nexport function setColumnFilter(\n  filters: readonly ColumnFilter[],\n  columnId: string,\n  value: FilterValue | undefined,\n): ColumnFilter[] {\n  const rest = filters.filter((f) => f.id !== columnId);\n  return value !== undefined && isFilterActive(value) ? [...rest, { id: columnId, value }] : rest;\n}\n\n/** Unique values for a column — use to build `select` filter options. */\nexport function getFacetValues<T extends RowData>(rows: Row<T>[], columnId: string): string[] {\n  const set = new Set<string>();\n  for (const row of rows) {\n    const v = row.getValue(columnId);\n    if (v === null || v === undefined || v === \"\") continue;\n    if (Array.isArray(v)) for (const item of v) set.add(String(item));\n    else set.add(String(v));\n  }\n  return [...set].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));\n}\n"
    },
    {
      "path": "registry/tablekit/core/format.ts",
      "type": "registry:lib",
      "target": "components/tablekit/core/format.ts",
      "content": "/**\n * Locale-aware formatters used by schema columns. Pure functions — safe on server and client.\n */\n\nexport interface NumberFormatOptions {\n  locale?: string;\n  decimals?: number;\n  notation?: \"standard\" | \"compact\";\n  style?: \"decimal\" | \"percent\";\n  unit?: string;\n}\n\nexport interface CurrencyFormatOptions {\n  locale?: string;\n  currency?: string;\n  decimals?: number;\n  notation?: \"standard\" | \"compact\";\n}\n\nexport interface DateFormatOptions {\n  locale?: string;\n  dateStyle?: \"short\" | \"medium\" | \"long\" | \"relative\";\n  timeStyle?: \"short\" | \"medium\";\n}\n\nconst cache = new Map<string, Intl.NumberFormat | Intl.DateTimeFormat | Intl.RelativeTimeFormat>();\n\nfunction memo<F extends Intl.NumberFormat | Intl.DateTimeFormat | Intl.RelativeTimeFormat>(\n  key: string,\n  make: () => F,\n): F {\n  let f = cache.get(key) as F | undefined;\n  if (!f) {\n    f = make();\n    cache.set(key, f);\n  }\n  return f;\n}\n\nfunction toNumber(value: unknown): number | null {\n  if (value === null || value === undefined || value === \"\") return null;\n  const n = typeof value === \"number\" ? value : Number(value);\n  return Number.isNaN(n) ? null : n;\n}\n\nexport function toDate(value: unknown): Date | null {\n  if (value === null || value === undefined || value === \"\") return null;\n  const d = value instanceof Date ? value : new Date(value as string | number);\n  return Number.isNaN(d.getTime()) ? null : d;\n}\n\nexport function formatNumber(value: unknown, o: NumberFormatOptions = {}): string {\n  const n = toNumber(value);\n  if (n === null) return \"\";\n  const fmt = memo(`n|${o.locale}|${o.decimals}|${o.notation}|${o.style}|${o.unit}`, () =>\n    o.unit\n      ? new Intl.NumberFormat(o.locale, {\n          style: \"unit\",\n          unit: o.unit,\n          notation: o.notation,\n          maximumFractionDigits: o.decimals,\n          minimumFractionDigits: o.decimals,\n        })\n      : new Intl.NumberFormat(o.locale, {\n          style: o.style ?? \"decimal\",\n          notation: o.notation,\n          maximumFractionDigits: o.decimals,\n          minimumFractionDigits: o.decimals,\n        }),\n  );\n  return (fmt as Intl.NumberFormat).format(n);\n}\n\nexport function formatCurrency(value: unknown, o: CurrencyFormatOptions = {}): string {\n  const n = toNumber(value);\n  if (n === null) return \"\";\n  const fmt = memo(\n    `c|${o.locale}|${o.currency}|${o.decimals}|${o.notation}`,\n    () =>\n      new Intl.NumberFormat(o.locale, {\n        style: \"currency\",\n        currency: o.currency ?? \"USD\",\n        notation: o.notation,\n        maximumFractionDigits: o.decimals,\n        minimumFractionDigits: o.decimals,\n      }),\n  );\n  return (fmt as Intl.NumberFormat).format(n);\n}\n\nconst RELATIVE_UNITS: [Intl.RelativeTimeFormatUnit, number][] = [\n  [\"year\", 365 * 24 * 3600],\n  [\"month\", 30 * 24 * 3600],\n  [\"week\", 7 * 24 * 3600],\n  [\"day\", 24 * 3600],\n  [\"hour\", 3600],\n  [\"minute\", 60],\n  [\"second\", 1],\n];\n\nexport function formatRelative(date: Date, locale?: string, now: Date = new Date()): string {\n  const seconds = Math.round((date.getTime() - now.getTime()) / 1000);\n  const fmt = memo(`r|${locale}`, () => new Intl.RelativeTimeFormat(locale, { numeric: \"auto\" }));\n  for (const [unit, size] of RELATIVE_UNITS) {\n    if (Math.abs(seconds) >= size || unit === \"second\") {\n      return (fmt as Intl.RelativeTimeFormat).format(Math.round(seconds / size), unit);\n    }\n  }\n  return \"\";\n}\n\nexport function formatDate(value: unknown, o: DateFormatOptions = {}): string {\n  const d = toDate(value);\n  if (!d) return \"\";\n  const { dateStyle } = o;\n  if (dateStyle === \"relative\") return formatRelative(d, o.locale);\n  const fmt = memo(\n    `d|${o.locale}|${dateStyle}|${o.timeStyle}`,\n    () =>\n      new Intl.DateTimeFormat(o.locale, {\n        dateStyle: dateStyle ?? \"medium\",\n        timeStyle: o.timeStyle,\n      }),\n  );\n  return (fmt as Intl.DateTimeFormat).format(d);\n}\n\n/** Title-case a machine value for display: \"in_progress\" → \"In progress\". */\nexport function humanize(value: unknown): string {\n  const s = String(value ?? \"\")\n    .replace(/[_-]+/g, \" \")\n    .trim();\n  return s ? s.charAt(0).toUpperCase() + s.slice(1) : \"\";\n}\n"
    },
    {
      "path": "registry/tablekit/core/index.ts",
      "type": "registry:lib",
      "target": "components/tablekit/core/index.ts",
      "content": "export * from \"./badge-icons\";\nexport * from \"./columns\";\nexport * from \"./filtering\";\nexport * from \"./format\";\nexport * from \"./pagination\";\nexport type {\n  BadgeIndicator,\n  ColumnSchemaInput,\n  ColumnSchemaType,\n  ColumnType,\n  TableSchemaInput,\n  TableSchemaType,\n  Tone,\n} from \"./schema\";\nexport * from \"./schema-columns\";\nexport * from \"./selection\";\nexport * from \"./sorting\";\nexport * from \"./table\";\nexport * from \"./types\";\nexport { defaultGetRowId, functionalUpdate, getColumnValue } from \"./utils\";\n"
    },
    {
      "path": "registry/tablekit/core/pagination.ts",
      "type": "registry:lib",
      "target": "components/tablekit/core/pagination.ts",
      "content": "import type { PaginationState } from \"./types\";\nimport { clamp } from \"./utils\";\n\nexport const DEFAULT_PAGE_SIZE = 10;\n\nexport function getPageCount(totalRows: number, pageSize: number): number {\n  if (pageSize <= 0) return 1;\n  return Math.max(1, Math.ceil(totalRows / pageSize));\n}\n\nexport function clampPagination(state: PaginationState, totalRows: number): PaginationState {\n  const pageCount = getPageCount(totalRows, state.pageSize);\n  const pageIndex = clamp(state.pageIndex, 0, pageCount - 1);\n  return pageIndex === state.pageIndex ? state : { ...state, pageIndex };\n}\n\nexport function paginate<R>(rows: R[], state: PaginationState): R[] {\n  const start = state.pageIndex * state.pageSize;\n  return rows.slice(start, start + state.pageSize);\n}\n\n/** 1-based \"Showing X–Y of Z\" numbers. */\nexport function getPageRange(\n  state: PaginationState,\n  totalRows: number,\n): { from: number; to: number; total: number } {\n  if (totalRows === 0) return { from: 0, to: 0, total: 0 };\n  const from = state.pageIndex * state.pageSize + 1;\n  const to = Math.min(totalRows, from + state.pageSize - 1);\n  return { from, to, total: totalRows };\n}\n\n/**\n * Compact page list with ellipses, e.g. [1, \"…\", 4, 5, 6, \"…\", 12].\n * Pages are 1-based for display.\n */\nexport function getPageItems(pageIndex: number, pageCount: number, siblings = 1): (number | \"…\")[] {\n  const current = pageIndex + 1;\n  const total = pageCount;\n  const window = siblings * 2 + 5;\n  if (total <= window) return Array.from({ length: total }, (_, i) => i + 1);\n\n  const left = Math.max(current - siblings, 2);\n  const right = Math.min(current + siblings, total - 1);\n  const items: (number | \"…\")[] = [1];\n  if (left > 2) items.push(\"…\");\n  for (let p = left; p <= right; p++) items.push(p);\n  if (right < total - 1) items.push(\"…\");\n  items.push(total);\n  return items;\n}\n"
    },
    {
      "path": "registry/tablekit/core/schema-columns.ts",
      "type": "registry:lib",
      "target": "components/tablekit/core/schema-columns.ts",
      "content": "/**\n * Turn a declarative TableSchema into ColumnDefs — without pulling zod into the bundle.\n * `resolveSchema` applies the same defaults as the zod schema (a test keeps them in sync).\n */\nimport { toDate } from \"./format\";\nimport type { ColumnSchemaType, TableSchemaInput, TableSchemaType } from \"./schema\";\nimport type { Align, ColumnDef, FilterKind, RowData } from \"./types\";\n\nexport function getByPath(row: unknown, path: string): unknown {\n  if (row === null || row === undefined) return undefined;\n  if (!path.includes(\".\")) return (row as Record<string, unknown>)[path];\n  let cur: unknown = row;\n  for (const key of path.split(\".\")) {\n    if (cur === null || cur === undefined) return undefined;\n    cur = (cur as Record<string, unknown>)[key];\n  }\n  return cur;\n}\n\n/** Replace `{field}` placeholders with (URL-encoded) row values. */\nexport function fillTemplate(template: string, row: unknown): string {\n  return template.replace(/\\{([^}]+)\\}/g, (_, key: string) =>\n    encodeURIComponent(String(getByPath(row, key.trim()) ?? \"\")),\n  );\n}\n\nexport function resolveSchema(input: TableSchemaInput): TableSchemaType {\n  const f = input.features ?? {};\n  const a = input.appearance ?? {};\n  return {\n    ...input,\n    version: 1,\n    columns: input.columns.map((c) => ({ ...c, type: c.type ?? \"text\" })),\n    features: {\n      search: f.search ?? true,\n      columnFilters: f.columnFilters ?? true,\n      sorting: f.sorting ?? true,\n      multiSort: f.multiSort ?? true,\n      pagination: f.pagination ?? true,\n      pageSize: f.pageSize ?? 10,\n      pageSizeOptions: f.pageSizeOptions ?? [10, 25, 50, 100],\n      selection: f.selection ?? \"none\",\n      columnVisibility: f.columnVisibility ?? true,\n      columnResize: f.columnResize ?? true,\n      columnReorder: f.columnReorder ?? true,\n      columnPinning: f.columnPinning ?? true,\n      stickyHeader: f.stickyHeader ?? true,\n    },\n    appearance: {\n      density: a.density ?? \"default\",\n      variant: a.variant ?? \"plain\",\n      responsive: a.responsive ?? \"stack\",\n      stackBelow: a.stackBelow ?? 640,\n    },\n    initialState: input.initialState\n      ? {\n          ...input.initialState,\n          sort: input.initialState.sort?.map((s) => ({ id: s.id, desc: s.desc ?? false })),\n        }\n      : undefined,\n  } as TableSchemaType;\n}\n\nfunction defaultFilter(type: ColumnSchemaType[\"type\"]): FilterKind | false {\n  switch (type) {\n    case \"badge\":\n    case \"boolean\":\n      return \"select\";\n    case \"number\":\n    case \"currency\":\n    case \"date\":\n      return \"range\";\n    default:\n      return false;\n  }\n}\n\nfunction defaultAlign(type: ColumnSchemaType[\"type\"]): Align {\n  if (type === \"number\" || type === \"currency\") return \"end\";\n  if (type === \"boolean\") return \"center\";\n  if (type === \"actions\") return \"end\";\n  return \"start\";\n}\n\n/** Width an actions column needs for its layout: icon buttons are 32px, text buttons ~7px/char. */\nexport function actionsWidth(c: ColumnSchemaType): number {\n  const actions = c.actions ?? [];\n  const pad = 16;\n  const textButton = (label: string) => Math.ceil(label.length * 7.2) + 22;\n  if (c.type === \"button\") {\n    // Button cells keep normal cell padding (2 × 16px) around a 1px-bordered button.\n    const label = c.button?.label ?? c.header;\n    return 34 + textButton(label) + (c.button?.icon ? 20 : 0);\n  }\n  if (c.actionsDisplay === \"inline\")\n    return pad + actions.length * 32 + Math.max(0, actions.length - 1) * 2;\n  return 56;\n}\n\nconst dateSort = (a: unknown, b: unknown) =>\n  (toDate(a)?.getTime() ?? 0) - (toDate(b)?.getTime() ?? 0);\n\nexport function schemaToColumns<T extends RowData = RowData>(\n  schema: TableSchemaType,\n): ColumnDef<T>[] {\n  const featureSort = schema.features.sorting;\n  const featureFilter = schema.features.columnFilters;\n  const featureResize = schema.features.columnResize;\n\n  return schema.columns.map((c): ColumnDef<T> => {\n    const isActions = c.type === \"actions\";\n    const isButton = c.type === \"button\";\n    return {\n      id: c.id ?? c.field,\n      header: c.header,\n      accessor: (row: T) => getByPath(row, c.field),\n      sortable: featureSort && !isActions && !isButton && c.sortable !== false,\n      sortFn: c.type === \"date\" ? dateSort : undefined,\n      filter:\n        featureFilter && !isActions && !isButton ? (c.filter ?? defaultFilter(c.type)) : false,\n      searchable: !isActions && !isButton && c.searchable !== false,\n      hideable: !isActions && c.hideable !== false,\n      resizable: featureResize && !isActions,\n      width: c.width ?? (isActions || isButton ? actionsWidth(c) : undefined),\n      minWidth: c.minWidth,\n      maxWidth: c.maxWidth,\n      pinned: c.pinned,\n      // Row actions stay where they are declared (normally last) and never pin.\n      pinnable: schema.features.columnPinning && !isActions,\n      reorderable: schema.features.columnReorder && !isActions,\n      priority: c.priority,\n      align: c.align ?? defaultAlign(c.type),\n      meta: { schema: c, kind: c.type },\n    };\n  });\n}\n"
    },
    {
      "path": "registry/tablekit/core/schema.ts",
      "type": "registry:lib",
      "target": "components/tablekit/core/schema.ts",
      "content": "/**\n * Declarative table schema — the contract agents write against.\n *\n * Import from `@tablekit/core/schema` (this entry depends on zod; the main entry does not).\n * A JSON Schema version ships as `@tablekit/core/tablekit.schema.json`.\n */\nimport { z } from \"zod\";\nimport { ACTION_ICONS, ACTIONS_DISPLAYS, BADGE_ICONS } from \"./badge-icons.ts\";\n\nexport const COLUMN_TYPES = [\n  \"text\",\n  \"number\",\n  \"currency\",\n  \"date\",\n  \"badge\",\n  \"avatar\",\n  \"link\",\n  \"boolean\",\n  \"actions\",\n  \"button\",\n] as const;\n\nexport const TONES = [\"neutral\", \"info\", \"success\", \"warning\", \"danger\", \"accent\"] as const;\n\nconst Tone = z\n  .enum(TONES)\n  .describe(\"Semantic color. Maps to --tk-tone-* tokens; never a raw color.\");\n\nconst Action = z\n  .object({\n    id: z.string().describe(\"Stable id passed to onAction / onBulkAction.\"),\n    label: z.string(),\n    tone: z.enum([\"neutral\", \"danger\"]).optional().describe(\"`danger` for destructive actions.\"),\n    icon: z\n      .enum(ACTION_ICONS)\n      .optional()\n      .describe(\"Lucide icon name. Required for every action when actionsDisplay is inline.\"),\n    disabled: z.boolean().optional().describe(\"Shown but not selectable.\"),\n    separator: z.boolean().optional().describe(\"Draw a divider before this item (menu only).\"),\n    when: z\n      .object({ field: z.string(), in: z.array(z.string()).min(1) })\n      .strict()\n      .optional()\n      .describe(\n        'Only show for rows where row[field] is one of `in`, e.g. { \"field\": \"status\", \"in\": [\"paid\"] }.',\n      ),\n  })\n  .strict();\n\nexport const ColumnSchema = z\n  .object({\n    field: z\n      .string()\n      .min(1)\n      .describe(\"Row property to read. Dot paths are supported: `customer.name`.\"),\n    header: z.string().describe(\"Visible column label.\"),\n    type: z\n      .enum(COLUMN_TYPES)\n      .default(\"text\")\n      .describe(\"Controls formatting, alignment, sorting and the default filter.\"),\n    id: z.string().optional().describe(\"Defaults to `field`. Must be unique.\"),\n\n    sortable: z.boolean().optional().describe(\"Default true (false for `actions`).\"),\n    filter: z\n      .union([z.enum([\"text\", \"select\", \"range\"]), z.literal(false)])\n      .optional()\n      .describe(\n        \"Column filter UI. Defaults: badge/boolean → select, number/currency/date → range, others → none.\",\n      ),\n    searchable: z.boolean().optional().describe(\"Include in global search. Default true.\"),\n    hideable: z.boolean().optional().describe(\"Show in the column visibility menu. Default true.\"),\n    hidden: z.boolean().optional().describe(\"Start hidden.\"),\n    width: z.number().int().positive().optional().describe(\"Initial width in px.\"),\n    minWidth: z.number().int().positive().optional(),\n    maxWidth: z.number().int().positive().optional(),\n    pinned: z\n      .boolean()\n      .optional()\n      .describe(\n        \"Initially pinned to the start edge (sticky when scrolling horizontally). Users can change it when features.columnPinning is on.\",\n      ),\n    priority: z\n      .number()\n      .int()\n      .min(1)\n      .max(5)\n      .optional()\n      .describe(\"1 = always visible … 5 = first to hide on narrow screens (responsive=priority).\"),\n    align: z.enum([\"start\", \"center\", \"end\"]).optional().describe(\"Numbers default to end.\"),\n\n    format: z\n      .object({\n        locale: z.string().optional().describe(\"BCP 47, e.g. `en-US`, `de-DE`.\"),\n        currency: z.string().length(3).optional().describe(\"ISO 4217 code, e.g. `USD`.\"),\n        decimals: z.number().int().min(0).max(8).optional(),\n        notation: z.enum([\"standard\", \"compact\"]).optional(),\n        style: z.enum([\"decimal\", \"percent\"]).optional(),\n        unit: z.string().optional().describe(\"Intl unit, e.g. `kilobyte`, `percent`.\"),\n        dateStyle: z.enum([\"short\", \"medium\", \"long\", \"relative\"]).optional(),\n        timeStyle: z.enum([\"short\", \"medium\"]).optional(),\n        trueLabel: z.string().optional(),\n        falseLabel: z.string().optional(),\n        prefix: z.string().optional(),\n        suffix: z.string().optional(),\n      })\n      .strict()\n      .optional(),\n\n    badge: z\n      .object({\n        tones: z\n          .record(z.string(), Tone)\n          .optional()\n          .describe('Value → tone, e.g. { \"paid\": \"success\", \"overdue\": \"danger\" }.'),\n        labels: z.record(z.string(), z.string()).optional().describe(\"Value → display label.\"),\n        defaultTone: Tone.optional(),\n        fill: z\n          .boolean()\n          .optional()\n          .describe(\"Tinted background. Default true. false + stroke = outline badge.\"),\n        stroke: z\n          .boolean()\n          .optional()\n          .describe(\n            \"1px border in the tone's colour. Default false. Combine with fill for a tinted, bordered badge.\",\n          ),\n        indicator: z\n          .enum([\"dot\", \"icon\", \"icon-only\", \"none\"])\n          .optional()\n          .describe(\n            'Leading mark. dot (default; \"icon\" when `icons` is set), icon = icon + label, icon-only = icon with the label as tooltip and screen-reader text, none = label only.',\n          ),\n        icons: z\n          .record(z.string(), z.enum(BADGE_ICONS))\n          .optional()\n          .describe(\n            'Value → Lucide icon name, e.g. { \"settled\": \"circle-check\", \"processing\": \"loader\" }. Values without one use their tone\\'s default icon.',\n          ),\n      })\n      .strict()\n      .optional()\n      .describe(\"Only for type=badge.\"),\n\n    avatar: z\n      .object({\n        imageField: z.string().optional().describe(\"Row field holding the image URL.\"),\n        imageDarkField: z\n          .string()\n          .optional()\n          .describe(\n            \"Row field holding a dark-theme variant of the image (e.g. a light logo for dark backgrounds). Shown instead of imageField when the table is dark.\",\n          ),\n        subtitleField: z.string().optional().describe(\"Row field shown under the name.\"),\n        logo: z\n          .enum([\"inline\", \"circle\"])\n          .optional()\n          .describe(\n            \"Render the image as a brand logo (bank, merchant, provider) instead of a person: inline = 16px compact mark centred on the name's first line, no container; circle = mark centred in a filled circle.\",\n          ),\n        logoFill: z\n          .enum([\"neutral\", \"accent\"])\n          .optional()\n          .describe(\"Fill behind logo=circle. Default neutral.\"),\n      })\n      .strict()\n      .optional()\n      .describe(\"Only for type=avatar. `field` is the display name.\"),\n\n    link: z\n      .object({\n        hrefField: z.string().optional().describe(\"Row field holding the URL.\"),\n        hrefTemplate: z\n          .string()\n          .optional()\n          .describe(\"URL with {field} placeholders, e.g. `/orders/{id}`.\"),\n        external: z.boolean().optional().describe(\"Open in a new tab with rel=noopener.\"),\n      })\n      .strict()\n      .optional()\n      .describe(\"Only for type=link.\"),\n\n    actions: z.array(Action).optional().describe(\"Only for type=actions. Row menu items.\"),\n    actionsDisplay: z\n      .enum(ACTIONS_DISPLAYS)\n      .optional()\n      .describe(\n        \"Only for type=actions. menu (default) = ⋯ menu; inline = icon buttons (every action needs an icon). For one clear call to action per row, use a type=button column instead.\",\n      ),\n\n    button: z\n      .object({\n        id: z.string().describe(\"Action id passed to onAction(id, row).\"),\n        label: z.string().optional().describe(\"Button text. Defaults to the cell value.\"),\n        variant: z\n          .enum([\"secondary\", \"primary\", \"ghost\"])\n          .optional()\n          .describe(\"secondary (default, outlined) · primary (accent fill) · ghost (text only).\"),\n        tone: z.enum([\"neutral\", \"danger\"]).optional(),\n        icon: z.enum(ACTION_ICONS).optional().describe(\"Leading Lucide icon.\"),\n        when: z\n          .object({ field: z.string(), in: z.array(z.string()).min(1) })\n          .strict()\n          .optional()\n          .describe(\n            \"Only render the button on rows where row[field] is one of `in`; other rows show nothing.\",\n          ),\n      })\n      .strict()\n      .optional()\n      .describe(\n        \"Only for type=button: a single button inside the cell (e.g. Pay, Download, Retry).\",\n      ),\n  })\n  .strict();\n\nexport const TableSchema = z\n  .object({\n    $schema: z.string().optional(),\n    version: z.literal(1).default(1),\n    title: z.string().optional().describe(\"Shown in the toolbar and used as the accessible name.\"),\n    description: z.string().optional(),\n    rowId: z.string().optional().describe(\"Row field used as a stable id. Default `id`.\"),\n    columns: z.array(ColumnSchema).min(1),\n\n    features: z\n      .object({\n        search: z.boolean().default(true).describe(\"Global search box.\"),\n        columnFilters: z.boolean().default(true),\n        sorting: z.boolean().default(true),\n        multiSort: z.boolean().default(true),\n        pagination: z.boolean().default(true),\n        pageSize: z.number().int().positive().default(10),\n        pageSizeOptions: z.array(z.number().int().positive()).default([10, 25, 50, 100]),\n        selection: z.enum([\"none\", \"single\", \"multi\"]).default(\"none\"),\n        columnVisibility: z.boolean().default(true),\n        columnResize: z.boolean().default(true),\n        columnReorder: z\n          .boolean()\n          .default(true)\n          .describe(\"Users can reorder columns from the column menu (drag or Alt+Arrow keys).\"),\n        columnPinning: z\n          .boolean()\n          .default(true)\n          .describe(\n            \"Users can pin columns to the start edge from the column menu. `column.pinned` sets the initial state.\",\n          ),\n        stickyHeader: z.boolean().default(true),\n      })\n      .strict()\n      .prefault({}),\n\n    appearance: z\n      .object({\n        density: z.enum([\"compact\", \"default\", \"comfortable\"]).default(\"default\"),\n        variant: z.enum([\"plain\", \"zebra\", \"bordered\"]).default(\"plain\"),\n        responsive: z\n          .enum([\"stack\", \"scroll\", \"priority\"])\n          .default(\"stack\")\n          .describe(\n            \"Narrow-screen behavior. stack = cards, scroll = horizontal scroll, priority = drop low-priority columns.\",\n          ),\n        stackBelow: z\n          .number()\n          .int()\n          .positive()\n          .default(640)\n          .describe(\"Container width (px) below which `stack` switches to cards.\"),\n      })\n      .strict()\n      .prefault({}),\n\n    initialState: z\n      .object({\n        sort: z\n          .array(z.object({ id: z.string(), desc: z.boolean().default(false) }).strict())\n          .optional(),\n        search: z.string().optional(),\n      })\n      .strict()\n      .optional(),\n\n    bulkActions: z\n      .array(Action)\n      .optional()\n      .describe(\"Shown in the selection bar when rows are selected. Requires selection=multi.\"),\n\n    emptyState: z\n      .object({ title: z.string(), description: z.string().optional() })\n      .strict()\n      .optional(),\n  })\n  .strict();\n\nexport type ColumnSchemaInput = z.input<typeof ColumnSchema>;\nexport type ColumnSchemaType = z.output<typeof ColumnSchema>;\nexport type TableSchemaInput = z.input<typeof TableSchema>;\nexport type TableSchemaType = z.output<typeof TableSchema>;\nexport type ColumnType = (typeof COLUMN_TYPES)[number];\nexport type Tone = (typeof TONES)[number];\nexport type BadgeIndicator = \"dot\" | \"icon\" | \"icon-only\" | \"none\";\n\nexport type ParseResult =\n  | { success: true; data: TableSchemaType }\n  | { success: false; errors: string[] };\n\n/**\n * Validate a schema and return human/agent-readable errors like\n * `columns.2.type: Invalid option: expected one of \"text\"|\"number\"…`.\n */\nexport function parseTableSchema(input: unknown): ParseResult {\n  const result = TableSchema.safeParse(input);\n  if (result.success) {\n    const errors = semanticErrors(result.data);\n    return errors.length ? { success: false, errors } : { success: true, data: result.data };\n  }\n  return {\n    success: false,\n    errors: result.error.issues.map((i) => `${i.path.join(\".\") || \"(root)\"}: ${i.message}`),\n  };\n}\n\nfunction semanticErrors(schema: TableSchemaType): string[] {\n  const errors: string[] = [];\n  const ids = new Set<string>();\n  schema.columns.forEach((c, i) => {\n    const id = c.id ?? c.field;\n    if (ids.has(id)) errors.push(`columns.${i}.id: duplicate column id \"${id}\"`);\n    ids.add(id);\n    if (c.badge && c.type !== \"badge\")\n      errors.push(`columns.${i}.badge: only valid when type=badge`);\n    if (c.link && c.type !== \"link\") errors.push(`columns.${i}.link: only valid when type=link`);\n    if (c.avatar && c.type !== \"avatar\")\n      errors.push(`columns.${i}.avatar: only valid when type=avatar`);\n    if (c.actions && c.type !== \"actions\")\n      errors.push(`columns.${i}.actions: only valid when type=actions`);\n    if (c.button && c.type !== \"button\")\n      errors.push(`columns.${i}.button: only valid when type=button`);\n    if (c.type === \"button\" && !c.button)\n      errors.push(`columns.${i}.button: required when type=button (at least { \"id\": \"…\" })`);\n    if (c.actionsDisplay && c.type !== \"actions\")\n      errors.push(`columns.${i}.actionsDisplay: only valid when type=actions`);\n    if (c.actionsDisplay === \"inline\")\n      c.actions?.forEach((a, j) => {\n        if (!a.icon)\n          errors.push(`columns.${i}.actions.${j}.icon: required when actionsDisplay is \"inline\"`);\n      });\n  });\n  if (schema.bulkActions?.length && schema.features.selection !== \"multi\") {\n    errors.push('bulkActions: requires features.selection = \"multi\"');\n  }\n  return errors;\n}\n\n/** JSON Schema (draft 2020-12) for editors, validators and LLM structured output. */\nexport function toJSONSchema(): Record<string, unknown> {\n  return z.toJSONSchema(TableSchema, { io: \"input\", target: \"draft-2020-12\" }) as Record<\n    string,\n    unknown\n  >;\n}\n"
    },
    {
      "path": "registry/tablekit/core/selection.ts",
      "type": "registry:lib",
      "target": "components/tablekit/core/selection.ts",
      "content": "import type { Row, RowData, SelectionMode } from \"./types\";\n\nexport type RowSelection = Record<string, boolean>;\n\nexport function toggleRow(\n  selection: RowSelection,\n  rowId: string,\n  mode: SelectionMode,\n  value?: boolean,\n): RowSelection {\n  if (mode === \"none\") return selection;\n  const next = value ?? !selection[rowId];\n  if (mode === \"single\") return next ? { [rowId]: true } : {};\n  const copy = { ...selection };\n  if (next) copy[rowId] = true;\n  else delete copy[rowId];\n  return copy;\n}\n\n/** Select or clear every row in `rows` (usually the current page), keeping other selections. */\nexport function toggleRows<T extends RowData>(\n  selection: RowSelection,\n  rows: readonly Row<T>[],\n  value: boolean,\n): RowSelection {\n  const copy = { ...selection };\n  for (const row of rows) {\n    if (value) copy[row.id] = true;\n    else delete copy[row.id];\n  }\n  return copy;\n}\n\nexport function getSelectionStatus<T extends RowData>(\n  selection: RowSelection,\n  rows: readonly Row<T>[],\n): \"none\" | \"some\" | \"all\" {\n  if (rows.length === 0) return \"none\";\n  let count = 0;\n  for (const row of rows) if (selection[row.id]) count++;\n  if (count === 0) return \"none\";\n  return count === rows.length ? \"all\" : \"some\";\n}\n\nexport function getSelectedIds(selection: RowSelection): string[] {\n  return Object.keys(selection).filter((id) => selection[id]);\n}\n\nexport function getSelectedRows<T extends RowData>(\n  selection: RowSelection,\n  rows: readonly Row<T>[],\n): Row<T>[] {\n  return rows.filter((r) => selection[r.id]);\n}\n"
    },
    {
      "path": "registry/tablekit/core/sorting.ts",
      "type": "registry:lib",
      "target": "components/tablekit/core/sorting.ts",
      "content": "import type { ColumnDef, Row, RowData, SortRule } from \"./types\";\nimport { toComparable } from \"./utils\";\n\nconst collator = new Intl.Collator(undefined, { numeric: true, sensitivity: \"base\" });\n\n/** Default comparator. Nulls/empties always sort last, regardless of direction (handled in sortRows). */\nexport function compareValues(a: unknown, b: unknown): number {\n  const ca = toComparable(a);\n  const cb = toComparable(b);\n  if (ca === cb) return 0;\n  if (ca === null) return 1;\n  if (cb === null) return -1;\n  if (typeof ca === \"number\" && typeof cb === \"number\") return ca - cb;\n  return collator.compare(String(ca), String(cb));\n}\n\nexport function sortRows<T extends RowData>(\n  rows: Row<T>[],\n  sorting: readonly SortRule[],\n  columns: readonly ColumnDef<T>[],\n): Row<T>[] {\n  if (sorting.length === 0) return rows;\n  const byId = new Map(columns.map((c) => [c.id, c]));\n  const rules = sorting.filter((r) => byId.has(r.id));\n  if (rules.length === 0) return rows;\n\n  // Array.prototype.sort is stable; fall back to original index for determinism.\n  return [...rows].sort((ra, rb) => {\n    for (const rule of rules) {\n      const column = byId.get(rule.id) as ColumnDef<T>;\n      const va = ra.getValue(rule.id);\n      const vb = rb.getValue(rule.id);\n      const aEmpty = toComparable(va) === null;\n      const bEmpty = toComparable(vb) === null;\n      // Keep empties at the bottom in both directions.\n      if (aEmpty !== bEmpty) return aEmpty ? 1 : -1;\n      const result = column.sortFn\n        ? column.sortFn(va, vb, ra.original, rb.original)\n        : compareValues(va, vb);\n      if (result !== 0) return rule.desc ? -result : result;\n    }\n    return ra.index - rb.index;\n  });\n}\n\n/**\n * Cycle a column's sort: none → asc → desc → none.\n * With `multi`, other rules are kept (shift+click); otherwise they're replaced.\n */\nexport function toggleSort(\n  sorting: readonly SortRule[],\n  columnId: string,\n  multi = false,\n): SortRule[] {\n  const existing = sorting.find((r) => r.id === columnId);\n  let next: SortRule | null;\n  if (!existing) next = { id: columnId, desc: false };\n  else if (!existing.desc) next = { id: columnId, desc: true };\n  else next = null;\n\n  if (!multi) return next ? [next] : [];\n  const rest = sorting.filter((r) => r.id !== columnId);\n  return next ? [...rest, next] : rest;\n}\n\nexport function getSortDirection(\n  sorting: readonly SortRule[],\n  columnId: string,\n): \"asc\" | \"desc\" | false {\n  const rule = sorting.find((r) => r.id === columnId);\n  if (!rule) return false;\n  return rule.desc ? \"desc\" : \"asc\";\n}\n\n/** 1-based position in a multi-sort, or 0 if unsorted. */\nexport function getSortIndex(sorting: readonly SortRule[], columnId: string): number {\n  return sorting.findIndex((r) => r.id === columnId) + 1;\n}\n"
    },
    {
      "path": "registry/tablekit/core/table.ts",
      "type": "registry:lib",
      "target": "components/tablekit/core/table.ts",
      "content": "import { filterRows } from \"./filtering\";\nimport { clampPagination, DEFAULT_PAGE_SIZE, getPageCount, paginate } from \"./pagination\";\nimport { sortRows } from \"./sorting\";\nimport type { RowData, RowModel, TableOptions, TableState, Updater } from \"./types\";\nimport { buildRows, functionalUpdate } from \"./utils\";\n\nexport function createInitialState(initial: Partial<TableState> = {}): TableState {\n  return {\n    sorting: [],\n    globalFilter: \"\",\n    columnFilters: [],\n    pagination: { pageIndex: 0, pageSize: DEFAULT_PAGE_SIZE },\n    rowSelection: {},\n    columnVisibility: {},\n    columnSizing: {},\n    columnOrder: [],\n    columnPinning: {},\n    ...initial,\n  };\n}\n\n/**\n * The whole pipeline as one pure function: rows → filter → sort → paginate.\n * Every adapter (React, Vue, vanilla) calls this.\n */\nexport function getRowModel<T extends RowData>(\n  options: TableOptions<T>,\n  state: TableState,\n): RowModel<T> {\n  const allRows = buildRows(options);\n  const filtered = options.manualFiltering\n    ? allRows\n    : filterRows(allRows, options.columns, state.globalFilter, state.columnFilters);\n  const sorted = options.manualSorting\n    ? filtered\n    : sortRows(filtered, state.sorting, options.columns);\n\n  const paginationOn = options.enablePagination !== false;\n  const totalRows = options.manualPagination ? (options.rowCount ?? sorted.length) : sorted.length;\n\n  if (!paginationOn) {\n    return { rows: sorted, filteredRows: sorted, allRows, pageCount: 1, totalRows };\n  }\n\n  const pageCount = getPageCount(totalRows, state.pagination.pageSize);\n  const rows = options.manualPagination\n    ? sorted\n    : paginate(sorted, clampPagination(state.pagination, totalRows));\n\n  return { rows, filteredRows: sorted, allRows, pageCount, totalRows };\n}\n\nexport type Listener = (state: TableState) => void;\n\nexport interface TableStore<T extends RowData> {\n  getState(): TableState;\n  setState(updater: Updater<TableState>): void;\n  getRowModel(): RowModel<T>;\n  setOptions(options: TableOptions<T>): void;\n  subscribe(listener: Listener): () => void;\n}\n\n/**\n * Framework-agnostic store. React users should prefer `useTable` from `@tablekit/react`.\n *\n * ```ts\n * const table = createTable({ data, columns });\n * table.subscribe(render);\n * table.setState((s) => ({ ...s, sorting: toggleSort(s.sorting, \"name\") }));\n * ```\n */\nexport function createTable<T extends RowData>(\n  options: TableOptions<T>,\n  initialState?: Partial<TableState>,\n): TableStore<T> {\n  let opts = options;\n  let state = createInitialState(initialState);\n  let cached: RowModel<T> | null = null;\n  const listeners = new Set<Listener>();\n\n  return {\n    getState: () => state,\n    setState(updater) {\n      state = functionalUpdate(updater, state);\n      cached = null;\n      for (const l of listeners) l(state);\n    },\n    getRowModel() {\n      if (!cached) cached = getRowModel(opts, state);\n      return cached;\n    },\n    setOptions(next) {\n      opts = next;\n      cached = null;\n    },\n    subscribe(listener) {\n      listeners.add(listener);\n      return () => listeners.delete(listener);\n    },\n  };\n}\n"
    },
    {
      "path": "registry/tablekit/core/types.ts",
      "type": "registry:lib",
      "target": "components/tablekit/core/types.ts",
      "content": "/** Any object can be a row. */\nexport type RowData = object;\n\nexport type Align = \"start\" | \"center\" | \"end\";\n\nexport type FilterKind = \"text\" | \"select\" | \"range\";\n\nexport interface RangeFilterValue {\n  min?: number | string | null;\n  max?: number | string | null;\n}\n\nexport type FilterValue = string | string[] | RangeFilterValue;\n\nexport interface ColumnDef<T extends RowData = RowData> {\n  /** Unique, stable id. Used as the state key for sorting, filters, visibility and sizing. */\n  id: string;\n  /** Visible header label. Falls back to `id`. */\n  header?: string;\n  /** Property name on the row, or a function that derives the cell value. Defaults to `id`. */\n  accessor?: keyof T | ((row: T) => unknown);\n  /** Default `true`. */\n  sortable?: boolean;\n  /** Custom comparator. Return <0, 0, >0. Receives the raw accessor values. */\n  sortFn?: (a: unknown, b: unknown, rowA: T, rowB: T) => number;\n  /** Which filter UI the column offers. `false`/undefined = not column-filterable. */\n  filter?: FilterKind | false;\n  /** Custom column filter predicate. */\n  filterFn?: (value: unknown, filterValue: FilterValue, row: T) => boolean;\n  /** Include this column in the global search. Default `true`. */\n  searchable?: boolean;\n  /** Can the user hide this column from the column menu. Default `true`. */\n  hideable?: boolean;\n  /** Can the user resize this column. Default `true`. */\n  resizable?: boolean;\n  /** Initial width in px. */\n  width?: number;\n  minWidth?: number;\n  maxWidth?: number;\n  /** Pin to the start edge (sticky while scrolling horizontally). Initial value; users can change it. */\n  pinned?: boolean;\n  /** Can the user pin or unpin this column from the column menu. Default `true`. */\n  pinnable?: boolean;\n  /** Can the user move this column from the column menu. Default `true`. */\n  reorderable?: boolean;\n  /** Lower number = more important. Used by `responsive=\"priority\"` to drop columns on narrow screens. */\n  priority?: number;\n  align?: Align;\n  /** Free-form data for renderers (e.g. schema column config). */\n  meta?: Record<string, unknown>;\n}\n\nexport interface SortRule {\n  id: string;\n  desc: boolean;\n}\n\nexport interface ColumnFilter {\n  id: string;\n  value: FilterValue;\n}\n\nexport interface PaginationState {\n  pageIndex: number;\n  pageSize: number;\n}\n\nexport type SelectionMode = \"none\" | \"single\" | \"multi\";\n\nexport interface TableState {\n  sorting: SortRule[];\n  globalFilter: string;\n  columnFilters: ColumnFilter[];\n  pagination: PaginationState;\n  /** Keyed by row id. */\n  rowSelection: Record<string, boolean>;\n  /** Keyed by column id. `false` = hidden. Missing = visible. */\n  columnVisibility: Record<string, boolean>;\n  /** Keyed by column id, width in px. */\n  columnSizing: Record<string, number>;\n  /** Column ids in display order. Empty = declaration order; ids not listed go at the end. */\n  columnOrder: string[];\n  /** Keyed by column id. Overrides `ColumnDef.pinned`; missing = use the definition. */\n  columnPinning: Record<string, boolean>;\n}\n\nexport type Updater<S> = S | ((prev: S) => S);\n\nexport interface TableOptions<T extends RowData = RowData> {\n  data: readonly T[];\n  columns: readonly ColumnDef<T>[];\n  /** Stable row id. Defaults to `row.id` when present, otherwise the row index. */\n  getRowId?: (row: T, index: number) => string;\n  selectionMode?: SelectionMode;\n  /** Set when the server sorts. Rows are passed through untouched. */\n  manualSorting?: boolean;\n  /** Set when the server filters. */\n  manualFiltering?: boolean;\n  /** Set when the server paginates. `data` is treated as the current page. */\n  manualPagination?: boolean;\n  /** Total rows on the server. Required for `manualPagination` page counts. */\n  rowCount?: number;\n  /** Allow sorting by more than one column (shift+click). Default `true`. */\n  enableMultiSort?: boolean;\n  /** Pagination on/off. When off, all rows are returned on one page. Default `true`. */\n  enablePagination?: boolean;\n}\n\nexport interface Row<T extends RowData = RowData> {\n  id: string;\n  index: number;\n  original: T;\n  /** Raw value for a column id. */\n  getValue: (columnId: string) => unknown;\n}\n\nexport interface RowModel<T extends RowData = RowData> {\n  /** Rows on the current page, after filter + sort. */\n  rows: Row<T>[];\n  /** All rows after filtering + sorting (every page). */\n  filteredRows: Row<T>[];\n  /** All rows, untouched. */\n  allRows: Row<T>[];\n  pageCount: number;\n  /** Total rows after filtering (or `rowCount` in manual mode). */\n  totalRows: number;\n}\n"
    },
    {
      "path": "registry/tablekit/core/utils.ts",
      "type": "registry:lib",
      "target": "components/tablekit/core/utils.ts",
      "content": "import type { ColumnDef, Row, RowData, TableOptions, Updater } from \"./types\";\n\nexport function functionalUpdate<S>(updater: Updater<S>, prev: S): S {\n  return typeof updater === \"function\" ? (updater as (p: S) => S)(prev) : updater;\n}\n\nexport function getColumnValue<T extends RowData>(row: T, column: ColumnDef<T>): unknown {\n  const { accessor } = column;\n  if (typeof accessor === \"function\") return accessor(row);\n  const key = (accessor ?? column.id) as keyof T;\n  return row[key];\n}\n\nexport function defaultGetRowId<T extends RowData>(row: T, index: number): string {\n  const id = (row as { id?: unknown }).id;\n  return id === undefined || id === null ? String(index) : String(id);\n}\n\nexport function buildRows<T extends RowData>(options: TableOptions<T>): Row<T>[] {\n  const getRowId = options.getRowId ?? defaultGetRowId;\n  const byId = new Map(options.columns.map((c) => [c.id, c]));\n  return options.data.map((original, index) => {\n    const cache = new Map<string, unknown>();\n    return {\n      id: getRowId(original, index),\n      index,\n      original,\n      getValue(columnId: string) {\n        if (cache.has(columnId)) return cache.get(columnId);\n        const column = byId.get(columnId);\n        const value = column\n          ? getColumnValue(original, column)\n          : (original as Record<string, unknown>)[columnId];\n        cache.set(columnId, value);\n        return value;\n      },\n    };\n  });\n}\n\n/** Convert a value into something comparable: number, timestamp or lowercase string. */\nexport function toComparable(value: unknown): number | string | null {\n  if (value === null || value === undefined || value === \"\") return null;\n  if (typeof value === \"number\") return Number.isNaN(value) ? null : value;\n  if (typeof value === \"boolean\") return value ? 1 : 0;\n  if (value instanceof Date) return value.getTime();\n  if (typeof value === \"string\") return value;\n  return String(value);\n}\n\nexport function clamp(n: number, min: number, max: number): number {\n  return Math.min(Math.max(n, min), max);\n}\n"
    },
    {
      "path": "registry/tablekit/DataTable.tsx",
      "type": "registry:component",
      "target": "components/tablekit/DataTable.tsx",
      "content": "\"use client\";\nimport {\n  type ColumnSchemaType,\n  humanize,\n  type Row,\n  type RowData,\n  resolveSchema,\n  schemaToColumns,\n  type TableSchemaInput,\n  type TableState,\n} from \"./core\";\nimport { type CSSProperties, type ReactNode, useMemo } from \"react\";\nimport { renderSchemaCell, schemaCellText } from \"./cells\";\nimport { Content } from \"./content\";\nimport { type Density, type Labels, Root } from \"./context\";\nimport { Pagination } from \"./pagination\";\nimport {\n  ColumnToggle,\n  FilterChips,\n  Filters,\n  Search,\n  SelectionBar,\n  SortSelect,\n  Toolbar,\n} from \"./toolbar\";\nimport { type ReactColumnDef, useTable } from \"./useTable\";\n\nexport interface DataTableProps<T extends RowData> {\n  /** Declarative config. Validate with `parseTableSchema` from `@tablekit/core/schema`. */\n  schema: TableSchemaInput;\n  data: readonly T[];\n  loading?: boolean;\n  error?: unknown;\n  onRetry?: () => void;\n  /** Row menu action (from `type: \"actions\"` columns). */\n  onAction?: (actionId: string, row: T) => void;\n  /** Selection-bar action (from `bulkActions`). */\n  onBulkAction?: (actionId: string, rows: T[]) => void;\n  onRowClick?: (row: T) => void;\n  onSelectionChange?: (ids: string[]) => void;\n  /** Controlled state (e.g. for server-side sorting/filtering/pagination). */\n  state?: Partial<TableState>;\n  onStateChange?: (state: TableState) => void;\n  /** Server mode: sorting, filtering and pagination are done by you. Pass `rowCount`. */\n  manual?: boolean;\n  rowCount?: number;\n  /** Override `appearance.density` from the schema. */\n  density?: Density;\n  theme?: \"light\" | \"dark\";\n  /** Scroll-area max height; the header sticks inside it. */\n  maxHeight?: number | string;\n  /** Extra toolbar controls, rendered after the built-in ones. */\n  toolbarExtra?: ReactNode;\n  labels?: Partial<Labels>;\n  className?: string;\n  style?: CSSProperties;\n}\n\n/**\n * Schema-driven table. The recommended entry point for AI agents and quick prototypes.\n *\n * ```tsx\n * <DataTable schema={{ columns: [{ field: \"name\", header: \"Name\" }] }} data={rows} />\n * ```\n */\nexport function DataTable<T extends RowData>(props: DataTableProps<T>) {\n  const { schema: input, data, onAction } = props;\n  const schema = useMemo(() => resolveSchema(input), [input]);\n\n  const columns = useMemo<ReactColumnDef<T>[]>(() => {\n    const byId = new Map<string, ColumnSchemaType>(schema.columns.map((c) => [c.id ?? c.field, c]));\n    return schemaToColumns<T>(schema).map((col) => {\n      const sc = byId.get(col.id) as ColumnSchemaType;\n      return {\n        ...col,\n        isActions: sc.type === \"actions\",\n        rangeType: sc.type === \"date\" ? \"date\" : \"number\",\n        optionLabel:\n          sc.type === \"badge\"\n            ? (v: string) => sc.badge?.labels?.[v] ?? humanize(v)\n            : sc.type === \"boolean\"\n              ? (v: string) =>\n                  v === \"true\" ? (sc.format?.trueLabel ?? \"Yes\") : (sc.format?.falseLabel ?? \"No\")\n              : undefined,\n        cell: ({ value, row }) =>\n          renderSchemaCell(sc, value, row.original, onAction as (id: string, r: RowData) => void),\n        text: ({ value, row }) => schemaCellText(sc, value, row.original),\n      };\n    });\n  }, [schema, onAction]);\n\n  // Initial state is read once, on mount.\n  // biome-ignore lint/correctness/useExhaustiveDependencies: intentional\n  const initialState = useMemo<Partial<TableState>>(\n    () => ({\n      sorting: schema.initialState?.sort ?? [],\n      globalFilter: schema.initialState?.search ?? \"\",\n      pagination: { pageIndex: 0, pageSize: schema.features.pageSize },\n      columnVisibility: Object.fromEntries(\n        schema.columns.filter((c) => c.hidden).map((c) => [c.id ?? c.field, false]),\n      ),\n    }),\n    [],\n  );\n\n  const rowIdField = schema.rowId;\n  const getRowId = useMemo(\n    () =>\n      rowIdField\n        ? (row: T, i: number) => String((row as Record<string, unknown>)[rowIdField] ?? i)\n        : undefined,\n    [rowIdField],\n  );\n\n  const table = useTable<T>({\n    data,\n    columns,\n    getRowId,\n    initialState,\n    state: props.state,\n    onStateChange: props.onStateChange,\n    onSelectionChange: props.onSelectionChange,\n    selectionMode: schema.features.selection,\n    enableMultiSort: schema.features.multiSort,\n    enablePagination: schema.features.pagination,\n    manualSorting: props.manual,\n    manualFiltering: props.manual,\n    manualPagination: props.manual,\n    rowCount: props.rowCount,\n  });\n\n  const f = schema.features;\n  const a = schema.appearance;\n  const hasToolbar =\n    schema.title ||\n    f.search ||\n    f.columnFilters ||\n    f.columnVisibility ||\n    f.columnReorder ||\n    f.columnPinning ||\n    props.toolbarExtra;\n\n  return (\n    <Root\n      table={table}\n      aria-label={schema.title ?? \"Data table\"}\n      density={props.density ?? a.density}\n      variant={a.variant}\n      responsive={a.responsive}\n      stackBelow={a.stackBelow}\n      stickyHeader={f.stickyHeader}\n      theme={props.theme}\n      labels={props.labels}\n      className={props.className}\n      style={props.style}\n    >\n      {hasToolbar && (\n        <Toolbar title={schema.title} description={schema.description}>\n          {f.search && <Search />}\n          <SortSelect />\n          {f.columnFilters && <Filters />}\n          {(f.columnVisibility || f.columnReorder || f.columnPinning) && (\n            <ColumnToggle\n              hide={f.columnVisibility}\n              reorder={f.columnReorder}\n              pin={f.columnPinning}\n            />\n          )}\n          {props.toolbarExtra}\n        </Toolbar>\n      )}\n      <FilterChips />\n      {f.selection === \"multi\" && (\n        <SelectionBar>\n          {schema.bulkActions?.map((action) => (\n            <button\n              key={action.id}\n              type=\"button\"\n              className=\"tk-button\"\n              data-tone={action.tone}\n              onClick={() =>\n                props.onBulkAction?.(\n                  action.id,\n                  table.selectedRows.map((r: Row<T>) => r.original),\n                )\n              }\n            >\n              {action.label}\n            </button>\n          ))}\n        </SelectionBar>\n      )}\n      <Content\n        loading={props.loading}\n        error={props.error}\n        onRetry={props.onRetry}\n        empty={schema.emptyState}\n        maxHeight={props.maxHeight}\n        onRowClick={\n          props.onRowClick ? (row: Row<T>) => props.onRowClick?.(row.original) : undefined\n        }\n      />\n      {f.pagination && <Pagination pageSizeOptions={f.pageSizeOptions} />}\n    </Root>\n  );\n}\n"
    },
    {
      "path": "registry/tablekit/cells.tsx",
      "type": "registry:component",
      "target": "components/tablekit/cells.tsx",
      "content": "\"use client\";\nimport {\n  type ActionIconName,\n  type ActionsDisplay,\n  type BadgeIconName,\n  type BadgeIndicator,\n  type ColumnSchemaType,\n  fillTemplate,\n  formatCurrency,\n  formatDate,\n  formatNumber,\n  getByPath,\n  humanize,\n  type RowData,\n  TONE_ICONS,\n  type Tone,\n  toDate,\n} from \"./core\";\nimport { Fragment, type ReactNode, useState } from \"react\";\nimport { useTableContext } from \"./context\";\nimport { ActionIcon, BadgeIcon, CheckIcon, ExternalIcon, MinusIcon, MoreIcon } from \"./icons\";\nimport { Popover } from \"./popover\";\n\nexport interface BadgeProps {\n  tone?: Tone;\n  /** dot (default) · icon · icon-only (label becomes tooltip + screen-reader text) · none */\n  indicator?: BadgeIndicator;\n  /** Lucide icon name for icon indicators. Defaults to the tone's icon (TONE_ICONS). */\n  icon?: BadgeIconName;\n  /** Tinted background. Default `true`. */\n  fill?: boolean;\n  /** 1px border in the tone's colour. Default `false`. `fill={false} stroke` = outline badge. */\n  stroke?: boolean;\n  children: ReactNode;\n}\n\nexport function Badge({\n  tone = \"neutral\",\n  indicator = \"dot\",\n  icon,\n  fill = true,\n  stroke = false,\n  children,\n}: BadgeProps) {\n  const iconName = icon ?? TONE_ICONS[tone];\n  const iconOnly = indicator === \"icon-only\";\n  return (\n    <span\n      className=\"tk-badge\"\n      data-tone={tone}\n      data-indicator={indicator === \"dot\" ? undefined : indicator}\n      data-fill={fill ? undefined : \"false\"}\n      data-stroke={stroke ? \"\" : undefined}\n      title={iconOnly && typeof children === \"string\" ? children : undefined}\n    >\n      {indicator === \"dot\" && <span className=\"tk-badge-dot\" aria-hidden=\"true\" />}\n      {(indicator === \"icon\" || iconOnly) && <BadgeIcon name={iconName} />}\n      {iconOnly ? <span className=\"tk-sr-only\">{children}</span> : children}\n    </span>\n  );\n}\n\nfunction initials(name: string): string {\n  const parts = name.trim().split(/\\s+/);\n  return (\n    (parts[0]?.[0] ?? \"\") + (parts.length > 1 ? (parts.at(-1)?.[0] ?? \"\") : \"\")\n  ).toUpperCase();\n}\n\nfunction AvatarImage({ name, src, srcDark }: { name: string; src?: string; srcDark?: string }) {\n  const [failed, setFailed] = useState(false);\n  // Initials whenever there's no image or it fails to load (offline, 404, blocked CDN).\n  if (!src || failed) return <>{initials(name)}</>;\n  const img = (url: string, scheme?: \"light\" | \"dark\") => (\n    <img\n      src={url}\n      alt=\"\"\n      loading=\"lazy\"\n      decoding=\"async\"\n      referrerPolicy=\"no-referrer\"\n      data-scheme={scheme}\n      onError={() => setFailed(true)}\n    />\n  );\n  if (!srcDark) return img(src);\n  // Both load; CSS shows the one matching the table's colour scheme (--tk-scheme-*-display).\n  return (\n    <>\n      {img(src, \"light\")}\n      {img(srcDark, \"dark\")}\n    </>\n  );\n}\n\nconst RING_COUNT = 8;\n\nfunction hashName(name: string): number {\n  let h = 0;\n  for (const ch of name) h = (h * 31 + ch.charCodeAt(0)) >>> 0;\n  return h;\n}\n\n/** Same name → same ring colour (1–8), on every render and every page. */\nfunction ringFor(name: string): number {\n  return (hashName(name) % RING_COUNT) + 1;\n}\n\n/**\n * Initials avatars take one of the theme's tones, picked by name, so a list of people\n * isn't a wall of one colour and follows whichever preset is loaded. `danger` is left\n * out so a person never reads as an error.\n */\nconst AVATAR_FALLBACK_TONES = [\"accent\", \"info\", \"success\", \"warning\", \"neutral\"] as const;\n\nfunction fallbackToneFor(name: string): (typeof AVATAR_FALLBACK_TONES)[number] {\n  // FNV-1a with a final mix: spreads similar names evenly across the few tones\n  // (the ring hash above clusters when there are only five buckets).\n  let h = 0x811c9dc5;\n  for (const ch of name) h = Math.imul(h ^ ch.charCodeAt(0), 0x01000193);\n  h ^= h >>> 16;\n  h = Math.imul(h, 0x45d9f3b);\n  h ^= h >>> 16;\n  return AVATAR_FALLBACK_TONES[(h >>> 0) % AVATAR_FALLBACK_TONES.length] ?? \"accent\";\n}\n\nexport function Avatar({\n  name,\n  src,\n  srcDark,\n  subtitle,\n  logo,\n  logoFill = \"neutral\",\n}: {\n  name: string;\n  src?: string;\n  /** Dark-theme variant of `src`, e.g. a light logo for dark backgrounds. */\n  srcDark?: string;\n  subtitle?: string;\n  /**\n   * Treat the image as a brand logo instead of a person:\n   * `inline` — compact mark exactly one line of the name tall, no container;\n   * `circle` — mark centred in a filled circle (`logoFill`).\n   */\n  logo?: \"inline\" | \"circle\";\n  logoFill?: \"neutral\" | \"accent\";\n}) {\n  return (\n    <span className=\"tk-avatar\" data-logo={logo}>\n      <span\n        className=\"tk-avatar-img\"\n        aria-hidden=\"true\"\n        data-logo={logo}\n        data-logo-fill={logo === \"circle\" ? logoFill : undefined}\n        data-has-image={src ? \"\" : undefined}\n        data-ring={src && !logo ? ringFor(name) : undefined}\n        data-tone={logo ? undefined : fallbackToneFor(name)}\n      >\n        <AvatarImage key={`${src}|${srcDark}`} name={name} src={src} srcDark={srcDark} />\n      </span>\n      <span className=\"tk-avatar-text\">\n        <span className=\"tk-avatar-name\">{name}</span>\n        {subtitle && <span className=\"tk-avatar-subtitle\">{subtitle}</span>}\n      </span>\n    </span>\n  );\n}\n\nexport interface ActionItem {\n  id: string;\n  label: string;\n  tone?: \"neutral\" | \"danger\";\n  icon?: ActionIconName;\n  disabled?: boolean;\n  /** Divider before this item (menus only). */\n  separator?: boolean;\n}\n\n/** The ⋯ menu. Items can carry an icon, be disabled, or start a new group. */\nexport function RowActions({\n  actions,\n  label,\n  onAction,\n}: {\n  actions: ActionItem[];\n  label: string;\n  onAction: (id: string) => void;\n}) {\n  const withIcons = actions.some((a) => a.icon);\n  return (\n    <Popover\n      role=\"menu\"\n      label={label}\n      align=\"end\"\n      className=\"tk-menu\"\n      trigger={(p) => (\n        <button type=\"button\" className=\"tk-icon-button\" aria-label={label} {...p}>\n          <MoreIcon />\n        </button>\n      )}\n    >\n      {(close) =>\n        actions.map((a, i) => (\n          <Fragment key={a.id}>\n            {a.separator && i > 0 && <hr className=\"tk-menu-separator\" />}\n            <button\n              type=\"button\"\n              role=\"menuitem\"\n              tabIndex={-1}\n              className=\"tk-menu-item\"\n              data-tone={a.tone}\n              disabled={a.disabled}\n              aria-disabled={a.disabled || undefined}\n              onClick={() => {\n                close();\n                onAction(a.id);\n              }}\n            >\n              {a.icon ? (\n                <ActionIcon name={a.icon} />\n              ) : (\n                withIcons && <span className=\"tk-menu-icon-space\" aria-hidden=\"true\" />\n              )}\n              <span>{a.label}</span>\n            </button>\n          </Fragment>\n        ))\n      }\n    </Popover>\n  );\n}\n\n/**\n * Row-action layouts. Multiple actions are never a row of text buttons:\n *  menu   — ⋯ menu (default)\n *  inline — icon buttons; each label is the tooltip and accessible name\n * For a single, clear call to action per row, use a `button` column (ButtonCell).\n */\nexport function RowActionsGroup({\n  actions,\n  display = \"menu\",\n  menuLabel,\n  onAction,\n}: {\n  actions: ActionItem[];\n  display?: ActionsDisplay;\n  menuLabel: string;\n  onAction: (id: string) => void;\n}) {\n  if (actions.length === 0) return null;\n  const iconsOk = display === \"inline\" && actions.every((a) => a.icon);\n  if (!iconsOk) return <RowActions actions={actions} label={menuLabel} onAction={onAction} />;\n  return (\n    <span className=\"tk-actions\" data-display=\"inline\">\n      {actions.map((a) => (\n        <button\n          key={a.id}\n          type=\"button\"\n          className=\"tk-icon-button\"\n          data-tone={a.tone}\n          aria-label={a.label}\n          title={a.label}\n          disabled={a.disabled}\n          onClick={() => onAction(a.id)}\n        >\n          {a.icon && <ActionIcon name={a.icon} />}\n        </button>\n      ))}\n    </span>\n  );\n}\n\nexport interface CellButtonProps {\n  label: string;\n  variant?: \"secondary\" | \"primary\" | \"ghost\";\n  tone?: \"neutral\" | \"danger\";\n  icon?: ActionIconName;\n  disabled?: boolean;\n  onClick: () => void;\n}\n\n/** A single button inside a cell (type=button columns): Pay, Download, Retry… */\nexport function CellButton({\n  label,\n  variant = \"secondary\",\n  tone,\n  icon,\n  disabled,\n  onClick,\n}: CellButtonProps) {\n  return (\n    <button\n      type=\"button\"\n      className=\"tk-cell-button\"\n      data-variant={variant}\n      data-tone={tone}\n      disabled={disabled}\n      onClick={onClick}\n    >\n      {icon && <ActionIcon name={icon} />}\n      {label}\n    </button>\n  );\n}\n\n/** Drop actions whose `when` condition doesn't match this row. */\nfunction visibleActions(column: ColumnSchemaType, row: RowData): ActionItem[] {\n  return (column.actions ?? []).filter(\n    (a) => !a.when || a.when.in.includes(String(getByPath(row, a.when.field) ?? \"\")),\n  );\n}\n\nfunction RowActionsCell({\n  column,\n  row,\n  onAction,\n}: {\n  column: ColumnSchemaType;\n  row: RowData;\n  onAction?: (actionId: string, row: RowData) => void;\n}) {\n  const { labels } = useTableContext();\n  const actions = visibleActions(column, row);\n  if (actions.length === 0) return null;\n  return (\n    <RowActionsGroup\n      actions={actions}\n      display={column.actionsDisplay}\n      menuLabel={labels.rowActions}\n      onAction={(id) => onAction?.(id, row)}\n    />\n  );\n}\n\n/** Plain-text rendering of a schema cell (search chips, card titles, aria-labels). */\nexport function schemaCellText(column: ColumnSchemaType, value: unknown, _row?: RowData): string {\n  const f = column.format ?? {};\n  const wrap = (s: string) => (s ? `${f.prefix ?? \"\"}${s}${f.suffix ?? \"\"}` : s);\n  switch (column.type) {\n    case \"number\":\n      return wrap(formatNumber(value, f));\n    case \"currency\":\n      return wrap(formatCurrency(value, f));\n    case \"date\":\n      return wrap(formatDate(value, f));\n    case \"boolean\":\n      return value ? (f.trueLabel ?? \"Yes\") : (f.falseLabel ?? \"No\");\n    case \"badge\": {\n      const key = String(value ?? \"\");\n      return column.badge?.labels?.[key] ?? humanize(key);\n    }\n    case \"actions\":\n      return \"\";\n    case \"button\":\n      return column.button?.label ?? \"\";\n    default:\n      return wrap(value === null || value === undefined ? \"\" : String(value));\n  }\n}\n\nexport function renderSchemaCell(\n  column: ColumnSchemaType,\n  value: unknown,\n  row: RowData,\n  onAction?: (actionId: string, row: RowData) => void,\n): ReactNode {\n  const f = column.format ?? {};\n  if (value === null || value === undefined || value === \"\") {\n    if (column.type === \"actions\")\n      return <RowActionsCell column={column} row={row} onAction={onAction} />;\n    if (column.type !== \"boolean\" && column.type !== \"button\")\n      return (\n        <span className=\"tk-empty-value\">\n          <span aria-hidden=\"true\">—</span>\n          <span className=\"tk-sr-only\">No value</span>\n        </span>\n      );\n  }\n  switch (column.type) {\n    case \"badge\": {\n      const key = String(value);\n      const b = column.badge;\n      const tone = b?.tones?.[key] ?? b?.defaultTone ?? \"neutral\";\n      const indicator: BadgeIndicator = b?.indicator ?? (b?.icons ? \"icon\" : \"dot\");\n      return (\n        <Badge\n          tone={tone}\n          indicator={indicator}\n          icon={b?.icons?.[key]}\n          fill={b?.fill ?? true}\n          stroke={b?.stroke ?? false}\n        >\n          {schemaCellText(column, value, row)}\n        </Badge>\n      );\n    }\n    case \"avatar\":\n      return (\n        <Avatar\n          name={String(value)}\n          src={\n            column.avatar?.imageField\n              ? (getByPath(row, column.avatar.imageField) as string)\n              : undefined\n          }\n          srcDark={\n            column.avatar?.imageDarkField\n              ? (getByPath(row, column.avatar.imageDarkField) as string | undefined)\n              : undefined\n          }\n          subtitle={\n            column.avatar?.subtitleField\n              ? (getByPath(row, column.avatar.subtitleField) as string | undefined)\n              : undefined\n          }\n          logo={column.avatar?.logo}\n          logoFill={column.avatar?.logoFill}\n        />\n      );\n    case \"link\": {\n      const href = column.link?.hrefTemplate\n        ? fillTemplate(column.link.hrefTemplate, row)\n        : column.link?.hrefField\n          ? String(getByPath(row, column.link.hrefField) ?? \"\")\n          : String(value);\n      const external = column.link?.external;\n      return (\n        <a\n          className=\"tk-link\"\n          href={href}\n          {...(external ? { target: \"_blank\", rel: \"noopener noreferrer\" } : {})}\n        >\n          {schemaCellText(column, value, row)}\n          {external && <ExternalIcon />}\n        </a>\n      );\n    }\n    case \"boolean\":\n      return (\n        <span className=\"tk-boolean\" data-value={value ? \"true\" : \"false\"}>\n          {value ? <CheckIcon /> : <MinusIcon />}\n          <span className=\"tk-sr-only\">{schemaCellText(column, value, row)}</span>\n        </span>\n      );\n    case \"date\": {\n      const d = toDate(value);\n      return d ? (\n        <time dateTime={d.toISOString()} title={d.toLocaleString(f.locale)}>\n          {schemaCellText(column, value, row)}\n        </time>\n      ) : (\n        String(value)\n      );\n    }\n    case \"number\":\n    case \"currency\":\n      return <span className=\"tk-num\">{schemaCellText(column, value, row)}</span>;\n    case \"actions\":\n      return <RowActionsCell column={column} row={row} onAction={onAction} />;\n    case \"button\": {\n      const b = column.button;\n      if (!b) return null;\n      if (b.when && !b.when.in.includes(String(getByPath(row, b.when.field) ?? \"\"))) return null;\n      return (\n        <CellButton\n          label={b.label ?? String(value ?? column.header)}\n          variant={b.variant}\n          tone={b.tone}\n          icon={b.icon}\n          onClick={() => onAction?.(b.id, row)}\n        />\n      );\n    }\n    default:\n      return schemaCellText(column, value, row);\n  }\n}\n"
    },
    {
      "path": "registry/tablekit/content.tsx",
      "type": "registry:component",
      "target": "components/tablekit/content.tsx",
      "content": "\"use client\";\nimport {\n  DEFAULT_AUTO_WIDTH,\n  DEFAULT_PINNED_WIDTH,\n  getColumnWidth,\n  getPinnedOffsets,\n  getRequiredWidth,\n  type Row,\n  type RowData,\n} from \"./core\";\nimport {\n  type CSSProperties,\n  type KeyboardEvent,\n  type MouseEvent,\n  type PointerEvent,\n  type ReactNode,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { type Labels, SELECT_WIDTH, useTableContext } from \"./context\";\nimport { AlertIcon, InboxIcon, SortAscIcon, SortDescIcon, SortNoneIcon } from \"./icons\";\nimport type { ReactColumnDef, TableInstance } from \"./useTable\";\n\nconst PINNED_FALLBACK = DEFAULT_PINNED_WIDTH;\nconst AUTO_WIDTH = DEFAULT_AUTO_WIDTH;\nconst INTERACTIVE =\n  \"button, a, input, select, textarea, label, [role='menuitem'], [role='separator']\";\n\nexport interface ContentProps<T extends RowData> {\n  loading?: boolean;\n  /** Truthy shows the error state. Pass an Error or a message. */\n  error?: unknown;\n  onRetry?: () => void;\n  /** Shown when there is no data at all (not when filters hide everything). */\n  empty?: ReactNode | { title: string; description?: string };\n  onRowClick?: (row: Row<T>) => void;\n  /** Max height of the scroll area. Needed for the sticky header to stick inside the table. */\n  maxHeight?: number | string;\n  /** Number of skeleton rows while loading with no data. Default: page size (max 8). */\n  skeletonRows?: number;\n}\n\nfunction renderCell<T extends RowData>(\n  table: TableInstance<T>,\n  column: ReactColumnDef<T>,\n  row: Row<T>,\n): ReactNode {\n  const value = row.getValue(column.id);\n  if (column.cell) return column.cell({ row, column, value, table });\n  if (value === null || value === undefined) return \"\";\n  if (value instanceof Date) return value.toLocaleDateString();\n  return String(value);\n}\n\nfunction cellText<T extends RowData>(\n  table: TableInstance<T>,\n  column: ReactColumnDef<T> | undefined,\n  row: Row<T>,\n): string {\n  if (!column) return row.id;\n  const value = row.getValue(column.id);\n  if (column.text) return column.text({ row, column, value, table });\n  const rendered = renderCell(table, column, row);\n  return typeof rendered === \"string\" ? rendered : String(value ?? row.id);\n}\n\nfunction Checkbox({\n  checked,\n  indeterminate = false,\n  label,\n  onChange,\n}: {\n  checked: boolean;\n  indeterminate?: boolean;\n  label: string;\n  onChange: (checked: boolean) => void;\n}) {\n  const ref = useRef<HTMLInputElement>(null);\n  useEffect(() => {\n    if (ref.current) ref.current.indeterminate = indeterminate;\n  }, [indeterminate]);\n  return (\n    <input\n      ref={ref}\n      type=\"checkbox\"\n      className=\"tk-checkbox\"\n      aria-label={label}\n      checked={checked}\n      onChange={(e) => onChange(e.target.checked)}\n    />\n  );\n}\n\n// ---- States ---------------------------------------------------------------\n\nfunction StatePanel({\n  icon,\n  title,\n  description,\n  action,\n  role,\n}: {\n  icon: ReactNode;\n  title: string;\n  description?: ReactNode;\n  action?: ReactNode;\n  role?: \"alert\" | \"status\";\n}) {\n  return (\n    <div className=\"tk-state\" role={role}>\n      <div className=\"tk-state-icon\">{icon}</div>\n      <p className=\"tk-state-title\">{title}</p>\n      {description && <p className=\"tk-state-description\">{description}</p>}\n      {action}\n    </div>\n  );\n}\n\nfunction useStateContent<T extends RowData>(props: ContentProps<T>): ReactNode | null {\n  const { table, labels } = useTableContext<T>();\n  if (props.error) {\n    const message =\n      props.error instanceof Error\n        ? props.error.message\n        : typeof props.error === \"string\"\n          ? props.error\n          : undefined;\n    return (\n      <StatePanel\n        role=\"alert\"\n        icon={<AlertIcon />}\n        title={labels.errorTitle}\n        description={message}\n        action={\n          props.onRetry && (\n            <button type=\"button\" className=\"tk-button\" onClick={props.onRetry}>\n              {labels.retry}\n            </button>\n          )\n        }\n      />\n    );\n  }\n  if (props.loading || table.rowModel.rows.length > 0) return null;\n  if (table.hasActiveFilters) {\n    return (\n      <StatePanel\n        role=\"status\"\n        icon={<InboxIcon />}\n        title={labels.noResults}\n        description={labels.noResultsDescription}\n        action={\n          <button type=\"button\" className=\"tk-button\" onClick={table.clearFilters}>\n            {labels.clearFilters}\n          </button>\n        }\n      />\n    );\n  }\n  const empty = props.empty;\n  if (empty && typeof empty === \"object\" && \"title\" in empty) {\n    return (\n      <StatePanel\n        role=\"status\"\n        icon={<InboxIcon />}\n        title={empty.title}\n        description={empty.description}\n      />\n    );\n  }\n  if (empty) return <div className=\"tk-state\">{empty}</div>;\n  return (\n    <StatePanel\n      role=\"status\"\n      icon={<InboxIcon />}\n      title={labels.emptyTitle}\n      description={labels.emptyDescription}\n    />\n  );\n}\n\n// ---- Header ---------------------------------------------------------------\n\nfunction sortAnnouncement(labels: Labels, name: string, dir: \"asc\" | \"desc\" | false) {\n  if (dir === \"asc\") return labels.sortedAsc(name);\n  if (dir === \"desc\") return labels.sortedDesc(name);\n  return labels.sortCleared;\n}\n\nfunction ResizeHandle<T extends RowData>({ column }: { column: ReactColumnDef<T> }) {\n  const { table, labels } = useTableContext<T>();\n  const start = useRef<{ x: number; w: number } | null>(null);\n  const name = column.header ?? column.id;\n  const width = getColumnWidth(column, table.state.columnSizing);\n\n  const onPointerDown = (e: PointerEvent<HTMLDivElement>) => {\n    const th = e.currentTarget.closest(\"th\");\n    if (!th) return;\n    e.preventDefault();\n    e.currentTarget.setPointerCapture(e.pointerId);\n    start.current = { x: e.clientX, w: th.getBoundingClientRect().width };\n  };\n  const onPointerMove = (e: PointerEvent<HTMLDivElement>) => {\n    if (!start.current) return;\n    table.resizeColumn(column.id, start.current.w + (e.clientX - start.current.x));\n  };\n  const onPointerUp = () => {\n    start.current = null;\n  };\n  const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n    const th = e.currentTarget.closest(\"th\");\n    const current = width ?? th?.getBoundingClientRect().width ?? AUTO_WIDTH;\n    const step = e.shiftKey ? 48 : 16;\n    if (e.key === \"ArrowLeft\" || e.key === \"ArrowRight\") {\n      e.preventDefault();\n      e.stopPropagation();\n      table.resizeColumn(column.id, current + (e.key === \"ArrowRight\" ? step : -step));\n    }\n  };\n\n  return (\n    // biome-ignore lint/a11y/useSemanticElements: a focusable, adjustable separator is the ARIA pattern for splitters\n    <div\n      role=\"separator\"\n      aria-orientation=\"vertical\"\n      aria-label={labels.resizeColumn(name)}\n      aria-valuenow={Math.round(width ?? AUTO_WIDTH)}\n      aria-valuemin={column.minWidth ?? 64}\n      aria-valuemax={column.maxWidth ?? 800}\n      tabIndex={0}\n      className=\"tk-resize\"\n      onPointerDown={onPointerDown}\n      onPointerMove={onPointerMove}\n      onPointerUp={onPointerUp}\n      onPointerCancel={onPointerUp}\n      onKeyDown={onKeyDown}\n      onDoubleClick={() =>\n        table.setState((s) => {\n          const { [column.id]: _, ...rest } = s.columnSizing;\n          return { ...s, columnSizing: rest };\n        })\n      }\n    />\n  );\n}\n\nfunction HeaderCell<T extends RowData>({\n  column,\n  pinnedLeft,\n}: {\n  column: ReactColumnDef<T>;\n  pinnedLeft?: number;\n}) {\n  const { table, labels, announce } = useTableContext<T>();\n  const name = column.header ?? column.id;\n  const dir = table.getSortDirection(column.id);\n  const index = table.getSortIndex(column.id);\n  const sortable = column.sortable !== false;\n  const style: CSSProperties | undefined =\n    pinnedLeft !== undefined ? { left: pinnedLeft } : undefined;\n\n  return (\n    <th\n      scope=\"col\"\n      className=\"tk-th\"\n      data-align={column.align}\n      data-pinned={pinnedLeft !== undefined || undefined}\n      data-sorted={dir || undefined}\n      data-actions={column.isActions || undefined}\n      aria-sort={dir === \"asc\" ? \"ascending\" : dir === \"desc\" ? \"descending\" : undefined}\n      style={style}\n    >\n      {sortable ? (\n        <button\n          type=\"button\"\n          className=\"tk-sort\"\n          onClick={(e) => {\n            table.toggleSort(column.id, e.shiftKey);\n            // Predict the next direction for the announcement.\n            const next = dir === false ? \"asc\" : dir === \"asc\" ? \"desc\" : false;\n            announce(sortAnnouncement(labels, name, next));\n          }}\n        >\n          <span className=\"tk-th-label\">{name}</span>\n          <span className=\"tk-sort-icon\" data-dir={dir || \"none\"}>\n            {dir === \"asc\" ? <SortAscIcon /> : dir === \"desc\" ? <SortDescIcon /> : <SortNoneIcon />}\n          </span>\n          {index > 0 && (\n            <span className=\"tk-sort-index\" aria-hidden=\"true\">\n              {index}\n            </span>\n          )}\n        </button>\n      ) : column.isActions ? (\n        <span className=\"tk-sr-only\">{name}</span>\n      ) : (\n        <span className=\"tk-th-label\">{name}</span>\n      )}\n      {column.resizable !== false && <ResizeHandle column={column} />}\n    </th>\n  );\n}\n\n// ---- Grid (table layout) ----------------------------------------------------\n\nfunction useGridKeyboard() {\n  return (e: KeyboardEvent<HTMLTableElement>) => {\n    const keys = [\"ArrowUp\", \"ArrowDown\", \"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"];\n    if (!keys.includes(e.key) || e.defaultPrevented) return;\n    const target = e.target as HTMLElement;\n    if (target.closest(\".tk-popover\")) return;\n    // Let inputs and separators keep their own arrow-key behavior.\n    if (target.matches(\"input:not([type='checkbox']), select, textarea, [role='separator']\"))\n      return;\n    const cell = target.closest<HTMLElement>(\"[data-r]\");\n    if (!cell) return;\n    const r = Number(cell.dataset.r);\n    const c = Number(cell.dataset.c);\n    let nr = r;\n    let nc = c;\n    if (e.key === \"ArrowUp\") nr--;\n    if (e.key === \"ArrowDown\") nr++;\n    if (e.key === \"ArrowLeft\") nc--;\n    if (e.key === \"ArrowRight\") nc++;\n    if (e.key === \"Home\") nc = 0;\n    if (e.key === \"End\") nc = Number.MAX_SAFE_INTEGER;\n    const table = e.currentTarget;\n    const rowCells = (row: number) => [...table.querySelectorAll<HTMLElement>(`[data-r=\"${row}\"]`)];\n    const cells = rowCells(nr);\n    if (cells.length === 0) return;\n    const next = cells[Math.min(Math.max(nc, 0), cells.length - 1)];\n    if (!next || next === cell) return;\n    e.preventDefault();\n    const inner = next.querySelector<HTMLElement>(INTERACTIVE);\n    (inner ?? next).focus();\n  };\n}\n\nfunction GridView<T extends RowData>(props: ContentProps<T>) {\n  const { table, columns, labels, label, stickyHeader } = useTableContext<T>();\n  const selection = table.options.selectionMode ?? \"none\";\n  const hasSelect = selection !== \"none\";\n  const anyPinned = columns.some((c) => c.pinned);\n  const offsets = anyPinned\n    ? getPinnedOffsets(\n        columns,\n        table.state.columnSizing,\n        hasSelect ? SELECT_WIDTH : 0,\n        PINNED_FALLBACK,\n      )\n    : {};\n  const state = useStateContent(props);\n  const onKeyDown = useGridKeyboard();\n  const [active, setActive] = useState<[number, number]>([0, 0]);\n\n  const widths = columns.map((c) => {\n    const w = getColumnWidth(c, table.state.columnSizing);\n    return w ?? (c.pinned ? PINNED_FALLBACK : undefined);\n  });\n  const minWidth = getRequiredWidth(\n    columns,\n    table.state.columnSizing,\n    hasSelect ? SELECT_WIDTH : 0,\n  );\n\n  const colCount = columns.length + (hasSelect ? 1 : 0);\n  const skeletonCount = props.skeletonRows ?? Math.min(table.state.pagination.pageSize || 5, 8);\n  const primary = columns.find((c) => !c.isActions);\n  const showSkeleton = props.loading && table.rowModel.rows.length === 0 && !props.error;\n\n  const onRowClick = (row: Row<T>) => (e: MouseEvent<HTMLTableRowElement>) => {\n    if (!props.onRowClick) return;\n    if ((e.target as HTMLElement).closest(INTERACTIVE)) return;\n    props.onRowClick(row);\n  };\n\n  return (\n    <div\n      className=\"tk-scroll\"\n      style={props.maxHeight !== undefined ? { maxHeight: props.maxHeight } : undefined}\n    >\n      <table\n        className=\"tk-table\"\n        aria-label={label}\n        aria-rowcount={table.rowModel.totalRows + 1}\n        aria-busy={props.loading || undefined}\n        data-sticky-header={stickyHeader || undefined}\n        style={{ minWidth }}\n        onKeyDown={onKeyDown}\n      >\n        <colgroup>\n          {hasSelect && <col style={{ width: SELECT_WIDTH }} />}\n          {columns.map((c, i) => (\n            <col key={c.id} style={widths[i] !== undefined ? { width: widths[i] } : undefined} />\n          ))}\n        </colgroup>\n        <thead>\n          <tr className=\"tk-tr\" aria-rowindex={1}>\n            {hasSelect && (\n              <th\n                scope=\"col\"\n                className=\"tk-th tk-select-cell\"\n                data-pinned={anyPinned || undefined}\n                style={anyPinned ? { left: 0 } : undefined}\n              >\n                {selection === \"multi\" ? (\n                  <Checkbox\n                    label={labels.selectAll}\n                    checked={table.pageSelection === \"all\"}\n                    indeterminate={table.pageSelection === \"some\"}\n                    onChange={(v) => table.togglePageSelected(v)}\n                  />\n                ) : (\n                  <span className=\"tk-sr-only\">{labels.selectAll}</span>\n                )}\n              </th>\n            )}\n            {columns.map((c) => (\n              <HeaderCell key={c.id} column={c} pinnedLeft={offsets[c.id]} />\n            ))}\n          </tr>\n        </thead>\n        <tbody>\n          {showSkeleton &&\n            Array.from({ length: skeletonCount }, (_, i) => (\n              // biome-ignore lint/suspicious/noArrayIndexKey: static placeholder rows\n              // biome-ignore lint/a11y/noAriaHiddenOnFocusable: placeholder rows contain nothing focusable\n              <tr key={i} className=\"tk-tr tk-skeleton-row\" aria-hidden=\"true\">\n                {Array.from({ length: colCount }, (_, j) => (\n                  // biome-ignore lint/suspicious/noArrayIndexKey: static placeholder cells\n                  <td key={j} className=\"tk-td\">\n                    <span\n                      className=\"tk-skeleton\"\n                      style={{ width: `${45 + ((i * 7 + j * 13) % 45)}%` }}\n                    />\n                  </td>\n                ))}\n              </tr>\n            ))}\n          {state && (\n            <tr className=\"tk-tr tk-state-row\">\n              <td className=\"tk-td\" colSpan={colCount}>\n                {state}\n              </td>\n            </tr>\n          )}\n          {!state &&\n            table.rowModel.rows.map((row, r) => {\n              const selected = !!table.state.rowSelection[row.id];\n              const rowLabel = cellText(table, primary, row);\n              return (\n                <tr\n                  key={row.id}\n                  className=\"tk-tr\"\n                  data-selected={selected || undefined}\n                  data-clickable={props.onRowClick ? true : undefined}\n                  aria-rowindex={\n                    table.state.pagination.pageIndex * table.state.pagination.pageSize + r + 2\n                  }\n                  onClick={onRowClick(row)}\n                >\n                  {hasSelect && (\n                    <td\n                      className=\"tk-td tk-select-cell\"\n                      data-pinned={anyPinned || undefined}\n                      style={anyPinned ? { left: 0 } : undefined}\n                      data-r={r}\n                      data-c={0}\n                    >\n                      <Checkbox\n                        label={labels.selectRow(rowLabel)}\n                        checked={selected}\n                        onChange={(v) => table.toggleRowSelected(row.id, v)}\n                      />\n                    </td>\n                  )}\n                  {columns.map((column, ci) => {\n                    const c = ci + (hasSelect ? 1 : 0);\n                    const isActive =\n                      Math.min(active[0], table.rowModel.rows.length - 1) === r &&\n                      Math.min(active[1], colCount - 1) === c;\n                    const pinnedLeft = offsets[column.id];\n                    return (\n                      <td\n                        key={column.id}\n                        className=\"tk-td\"\n                        data-align={column.align}\n                        data-actions={column.isActions || undefined}\n                        data-pinned={pinnedLeft !== undefined || undefined}\n                        style={pinnedLeft !== undefined ? { left: pinnedLeft } : undefined}\n                        data-r={r}\n                        data-c={c}\n                        tabIndex={isActive && !hasSelect ? 0 : -1}\n                        onFocus={() => setActive([r, c])}\n                        onKeyDown={(e) => {\n                          if (e.key === \"Enter\" && e.target === e.currentTarget && props.onRowClick)\n                            props.onRowClick(row);\n                        }}\n                      >\n                        {renderCell(table, column, row)}\n                      </td>\n                    );\n                  })}\n                </tr>\n              );\n            })}\n        </tbody>\n      </table>\n    </div>\n  );\n}\n\n// ---- Cards (stacked layout) -------------------------------------------------\n\nfunction CardsView<T extends RowData>(props: ContentProps<T>) {\n  const { table, columns, labels, label } = useTableContext<T>();\n  const selection = table.options.selectionMode ?? \"none\";\n  const state = useStateContent(props);\n  const actions = columns.find((c) => c.isActions);\n  const fields = columns.filter((c) => !c.isActions);\n  const [primary, ...rest] = fields;\n\n  if (props.loading && table.rowModel.rows.length === 0 && !props.error) {\n    return (\n      <ul className=\"tk-cards\" aria-busy=\"true\" aria-label={label}>\n        {Array.from({ length: 3 }, (_, i) => (\n          // biome-ignore lint/suspicious/noArrayIndexKey: static placeholder\n          <li key={i} className=\"tk-card\" aria-hidden=\"true\">\n            <span className=\"tk-skeleton\" style={{ width: \"60%\" }} />\n            <span className=\"tk-skeleton\" style={{ width: \"40%\" }} />\n            <span className=\"tk-skeleton\" style={{ width: \"75%\" }} />\n          </li>\n        ))}\n      </ul>\n    );\n  }\n  if (state) return <div className=\"tk-cards-state\">{state}</div>;\n\n  return (\n    <ul className=\"tk-cards\" aria-label={label}>\n      {table.rowModel.rows.map((row) => {\n        const selected = !!table.state.rowSelection[row.id];\n        const title = cellText(table, primary, row);\n        return (\n          <li\n            key={row.id}\n            className=\"tk-card\"\n            data-selected={selected || undefined}\n            data-clickable={props.onRowClick ? true : undefined}\n          >\n            <div className=\"tk-card-head\">\n              {selection !== \"none\" && (\n                <Checkbox\n                  label={labels.selectRow(title)}\n                  checked={selected}\n                  onChange={(v) => table.toggleRowSelected(row.id, v)}\n                />\n              )}\n              <div className=\"tk-card-title\">\n                {primary &&\n                  (props.onRowClick ? (\n                    <button\n                      type=\"button\"\n                      className=\"tk-card-open\"\n                      onClick={() => props.onRowClick?.(row)}\n                    >\n                      {renderCell(table, primary, row)}\n                    </button>\n                  ) : (\n                    renderCell(table, primary, row)\n                  ))}\n              </div>\n              {actions && <div className=\"tk-card-actions\">{renderCell(table, actions, row)}</div>}\n            </div>\n            {rest.length > 0 && (\n              <dl className=\"tk-card-fields\">\n                {rest.map((c) => (\n                  <div key={c.id} className=\"tk-card-field\">\n                    <dt>{c.header ?? c.id}</dt>\n                    <dd data-align={c.align}>{renderCell(table, c, row)}</dd>\n                  </div>\n                ))}\n              </dl>\n            )}\n          </li>\n        );\n      })}\n    </ul>\n  );\n}\n\n/** Renders the table, or cards in the stacked layout, including loading/empty/error states. */\nexport function Content<T extends RowData>(props: ContentProps<T>) {\n  const { layout } = useTableContext<T>();\n  return (\n    <div className=\"tk-content\" data-loading={props.loading || undefined}>\n      {props.loading && <div className=\"tk-progress\" aria-hidden=\"true\" />}\n      {layout === \"stack\" ? <CardsView {...props} /> : <GridView {...props} />}\n    </div>\n  );\n}\n"
    },
    {
      "path": "registry/tablekit/context.tsx",
      "type": "registry:component",
      "target": "components/tablekit/context.tsx",
      "content": "\"use client\";\nimport { fitColumnsToWidth, type RowData } from \"./core\";\nimport {\n  type CSSProperties,\n  createContext,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { useElementWidth } from \"./hooks\";\nimport type { ReactColumnDef, TableInstance } from \"./useTable\";\n\nexport type Density = \"compact\" | \"default\" | \"comfortable\";\nexport type Variant = \"plain\" | \"zebra\" | \"bordered\";\nexport type Responsive = \"stack\" | \"scroll\" | \"priority\";\nexport type Layout = \"table\" | \"stack\";\n\nexport const defaultLabels = {\n  search: \"Search\",\n  searchPlaceholder: \"Search…\",\n  filters: \"Filters\",\n  columns: \"Columns\",\n  toggleColumns: \"Show or hide columns\",\n  columnSettings: \"Show, hide, pin or reorder columns\",\n  pinnedColumns: \"Pinned\",\n  otherColumns: \"Columns\",\n  pinColumn: (col: string) => `Pin ${col}`,\n  moveColumn: (col: string) => `Move ${col}`,\n  moveColumnHint: \"Drag, or press the up and down arrow keys, to move.\",\n  columnMoved: (col: string, pos: number, total: number) =>\n    `${col} moved to position ${pos} of ${total}`,\n  columnPinned: (col: string) => `${col} pinned`,\n  columnUnpinned: (col: string) => `${col} unpinned`,\n  clearFilters: \"Clear filters\",\n  clearAll: \"Clear all\",\n  removeFilter: (name: string) => `Remove ${name} filter`,\n  selectAll: \"Select all rows on this page\",\n  selectRow: (label: string) => `Select ${label}`,\n  selected: (n: number) => `${n} selected`,\n  clearSelection: \"Clear selection\",\n  rowsPerPage: \"Rows per page\",\n  range: (from: number, to: number, total: number) =>\n    total === 0 ? \"0 results\" : `${from}–${to} of ${total}`,\n  previousPage: \"Previous page\",\n  nextPage: \"Next page\",\n  page: (n: number) => `Page ${n}`,\n  pageOf: (n: number, total: number) => `Page ${n} of ${total}`,\n  pagination: \"Pagination\",\n  noResults: \"No results\",\n  noResultsDescription: \"Try a different search, or clear the filters.\",\n  emptyTitle: \"Nothing here yet\",\n  emptyDescription: \"When there’s data, it will show up here.\",\n  errorTitle: \"Couldn’t load data\",\n  retry: \"Try again\",\n  loading: \"Loading…\",\n  sortedAsc: (col: string) => `Sorted by ${col}, ascending`,\n  sortedDesc: (col: string) => `Sorted by ${col}, descending`,\n  sortCleared: \"Sorting cleared\",\n  sortBy: \"Sort by\",\n  noSort: \"Default order\",\n  resizeColumn: (col: string) => `Resize ${col} column`,\n  rowActions: \"Row actions\",\n  min: \"Min\",\n  max: \"Max\",\n  any: \"Any\",\n  density: \"Density\",\n  densityCompact: \"Compact\",\n  densityDefault: \"Default\",\n  densityComfortable: \"Comfortable\",\n  results: (n: number) => `${n} ${n === 1 ? \"result\" : \"results\"}`,\n  yes: \"Yes\",\n  no: \"No\",\n};\n\nexport type Labels = typeof defaultLabels;\n\nexport interface TableContextValue<T extends RowData = RowData> {\n  table: TableInstance<T>;\n  density: Density;\n  setDensity: (d: Density) => void;\n  variant: Variant;\n  layout: Layout;\n  /** Columns to render right now (visibility + responsive priority applied). */\n  columns: ReactColumnDef<T>[];\n  stickyHeader: boolean;\n  labels: Labels;\n  label: string;\n  announce: (message: string) => void;\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: context is shared across row types\nconst TableContext = createContext<TableContextValue<any> | null>(null);\n\nexport function useTableContext<T extends RowData = RowData>(): TableContextValue<T> {\n  const ctx = useContext(TableContext);\n  if (!ctx) throw new Error(\"tablekit: <Table.*> parts must be rendered inside <Table.Root>.\");\n  return ctx as TableContextValue<T>;\n}\n\n/** Selection checkbox column width (px). Shared with content.tsx. */\nexport const SELECT_WIDTH = 44;\n\nexport interface RootProps<T extends RowData> {\n  table: TableInstance<T>;\n  /** Accessible name for the table. Required unless you render a visible title and pass it here. */\n  \"aria-label\": string;\n  density?: Density;\n  /** Uncontrolled starting density when `density` isn't passed. */\n  defaultDensity?: Density;\n  onDensityChange?: (d: Density) => void;\n  variant?: Variant;\n  /** Narrow-container behavior. Default `stack`. */\n  responsive?: Responsive;\n  /** Container width (px) below which `stack` switches to cards. Default 640. */\n  stackBelow?: number;\n  stickyHeader?: boolean;\n  /** Force a theme for this table only. Omit to follow the page. */\n  theme?: \"light\" | \"dark\";\n  labels?: Partial<Labels>;\n  className?: string;\n  style?: CSSProperties;\n  children: ReactNode;\n}\n\nexport function Root<T extends RowData>({\n  table,\n  \"aria-label\": label,\n  density: densityProp,\n  defaultDensity = \"default\",\n  onDensityChange,\n  variant = \"plain\",\n  responsive = \"stack\",\n  stackBelow = 640,\n  stickyHeader = true,\n  theme,\n  labels: labelOverrides,\n  className,\n  style,\n  children,\n}: RootProps<T>) {\n  const ref = useRef<HTMLDivElement>(null);\n  const width = useElementWidth(ref);\n  const [densityState, setDensityState] = useState<Density>(defaultDensity);\n  const density = densityProp ?? densityState;\n  const setDensity = useCallback(\n    (d: Density) => {\n      setDensityState(d);\n      onDensityChange?.(d);\n    },\n    [onDensityChange],\n  );\n\n  const [message, setMessage] = useState(\"\");\n  const announce = useCallback((m: string) => {\n    // Clear first so repeating the same message is still announced.\n    setMessage(\"\");\n    requestAnimationFrame(() => setMessage(m));\n  }, []);\n\n  const layout: Layout =\n    responsive === \"stack\" && width !== undefined && width < stackBelow ? \"stack\" : \"table\";\n\n  const columns = useMemo(() => {\n    if (responsive !== \"priority\" || width === undefined) return table.visibleColumns;\n    // Keep every column that fits; drop the least important ones only as needed.\n    const leading = (table.options.selectionMode ?? \"none\") !== \"none\" ? SELECT_WIDTH : 0;\n    return fitColumnsToWidth(table.visibleColumns, table.state.columnSizing, width - 2, leading);\n  }, [\n    responsive,\n    width,\n    table.visibleColumns,\n    table.state.columnSizing,\n    table.options.selectionMode,\n  ]);\n\n  const labels = useMemo(() => ({ ...defaultLabels, ...labelOverrides }), [labelOverrides]);\n\n  const value = useMemo<TableContextValue<T>>(\n    () => ({\n      table,\n      density,\n      setDensity,\n      variant,\n      layout,\n      columns,\n      stickyHeader,\n      labels,\n      label,\n      announce,\n    }),\n    [table, density, setDensity, variant, layout, columns, stickyHeader, labels, label, announce],\n  );\n\n  return (\n    <TableContext.Provider value={value}>\n      <div\n        ref={ref}\n        className={`tk-root${className ? ` ${className}` : \"\"}`}\n        style={style}\n        data-density={density}\n        data-variant={variant}\n        data-layout={layout}\n        data-tk-theme={theme}\n      >\n        {children}\n        <div className=\"tk-sr-only\" aria-live=\"polite\" aria-atomic=\"true\">\n          {message}\n        </div>\n      </div>\n    </TableContext.Provider>\n  );\n}\n"
    },
    {
      "path": "registry/tablekit/hooks.ts",
      "type": "registry:lib",
      "target": "components/tablekit/hooks.ts",
      "content": "\"use client\";\nimport { type RefObject, useEffect, useLayoutEffect, useState } from \"react\";\n\nconst useIsoLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\n/** Width of an element, tracked with ResizeObserver. `undefined` until measured (SSR-safe). */\nexport function useElementWidth(ref: RefObject<HTMLElement | null>): number | undefined {\n  const [width, setWidth] = useState<number | undefined>(undefined);\n  useIsoLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    // 0 means \"not laid out\" (display:none, jsdom) — treat as unmeasured.\n    const initial = el.getBoundingClientRect().width;\n    if (initial > 0) setWidth(initial);\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver((entries) => {\n      const w = entries[0]?.contentRect.width;\n      if (w) setWidth(w);\n    });\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [ref]);\n  return width;\n}\n\n/** Debounce a changing value. */\nexport function useDebounced<V>(value: V, ms: number): V {\n  const [v, setV] = useState(value);\n  useEffect(() => {\n    const t = setTimeout(() => setV(value), ms);\n    return () => clearTimeout(t);\n  }, [value, ms]);\n  return v;\n}\n"
    },
    {
      "path": "registry/tablekit/icons.tsx",
      "type": "registry:component",
      "target": "components/tablekit/icons.tsx",
      "content": "\"use client\";\n/**\n * Icons: Lucide (https://lucide.dev, ISC license) via lucide-react.\n *\n * The rest of the component imports these named wrappers, never lucide-react directly,\n * so swapping the icon set later is a one-file change. Every icon is decorative:\n * the surrounding button or cell carries the accessible name.\n */\nimport type { ActionIconName, BadgeIconName } from \"./core\";\nimport {\n  Archive,\n  ArrowDown,\n  ArrowRight,\n  ArrowUp,\n  Ban,\n  Check,\n  ChevronLeft,\n  ChevronRight,\n  ChevronsUpDown,\n  Circle,\n  CircleAlert,\n  CircleCheck,\n  CircleDashed,\n  CircleDot,\n  CirclePause,\n  CircleX,\n  Clock,\n  Columns3,\n  Copy,\n  Download,\n  Ellipsis,\n  ExternalLink,\n  Eye,\n  Flag,\n  GripVertical,\n  Hourglass,\n  Inbox,\n  Info,\n  ListFilter,\n  LoaderCircle,\n  Lock,\n  LockOpen,\n  type LucideIcon,\n  type LucideProps,\n  Mail,\n  Minus,\n  Pencil,\n  Pin,\n  Receipt,\n  RefreshCw,\n  Rows2,\n  Rows3,\n  Rows4,\n  Search,\n  Send,\n  Share2,\n  ShieldCheck,\n  Sparkles,\n  Star,\n  Trash2,\n  TriangleAlert,\n  Truck,\n  Undo2,\n  Upload,\n  UserPlus,\n  X,\n  Zap,\n} from \"lucide-react\";\n\ntype IconProps = Omit<LucideProps, \"ref\">;\n\n/** 16px, 1.75px stroke: Lucide's 24-unit grid scaled down, tuned for 12–14px text. */\nfunction wrap(Icon: LucideIcon, defaults: IconProps = {}) {\n  const Wrapped = (props: IconProps) => (\n    <Icon\n      size={16}\n      strokeWidth={1.75}\n      aria-hidden=\"true\"\n      focusable=\"false\"\n      className=\"tk-icon\"\n      {...defaults}\n      {...props}\n    />\n  );\n  Wrapped.displayName = `Tk${Icon.displayName ?? \"Icon\"}`;\n  return Wrapped;\n}\n\nexport const SortAscIcon = wrap(ArrowUp);\nexport const SortDescIcon = wrap(ArrowDown);\nexport const SortNoneIcon = wrap(ChevronsUpDown);\nexport const SearchIcon = wrap(Search);\nexport const FilterIcon = wrap(ListFilter);\nexport const ColumnsIcon = wrap(Columns3);\nexport const CloseIcon = wrap(X);\nexport const ChevronLeftIcon = wrap(ChevronLeft);\nexport const ChevronRightIcon = wrap(ChevronRight);\nexport const MoreIcon = wrap(Ellipsis);\nexport const CheckIcon = wrap(Check, { strokeWidth: 2.25 });\nexport const MinusIcon = wrap(Minus, { strokeWidth: 2.25 });\nexport const AlertIcon = wrap(CircleAlert);\nexport const InboxIcon = wrap(Inbox);\nexport const GripIcon = wrap(GripVertical);\nexport const PinIcon = wrap(Pin);\nexport const ExternalIcon = wrap(ExternalLink, { size: 12, strokeWidth: 2 });\n/** Density toggle: compact = 4 rows, default = 3, comfortable = 2. */\nexport const DensityCompactIcon = wrap(Rows4);\nexport const DensityDefaultIcon = wrap(Rows3);\nexport const DensityComfortableIcon = wrap(Rows2);\n\n/** Badge icon names (closed list in @tablekit/core) → Lucide components. */\nconst BADGE: Record<BadgeIconName, LucideIcon> = {\n  \"circle-check\": CircleCheck,\n  check: Check,\n  \"circle-x\": CircleX,\n  x: X,\n  \"triangle-alert\": TriangleAlert,\n  \"circle-alert\": CircleAlert,\n  info: Info,\n  \"circle-dashed\": CircleDashed,\n  circle: Circle,\n  \"circle-dot\": CircleDot,\n  \"circle-pause\": CirclePause,\n  clock: Clock,\n  hourglass: Hourglass,\n  loader: LoaderCircle,\n  \"refresh-cw\": RefreshCw,\n  ban: Ban,\n  lock: Lock,\n  \"shield-check\": ShieldCheck,\n  \"arrow-up\": ArrowUp,\n  \"arrow-down\": ArrowDown,\n  \"arrow-right\": ArrowRight,\n  \"undo-2\": Undo2,\n  send: Send,\n  truck: Truck,\n  star: Star,\n  zap: Zap,\n  sparkles: Sparkles,\n  eye: Eye,\n};\n\n/** Action icon names (closed list in @tablekit/core) → Lucide components. */\nconst ACTION: Record<ActionIconName, LucideIcon> = {\n  eye: Eye,\n  pencil: Pencil,\n  copy: Copy,\n  download: Download,\n  upload: Upload,\n  \"share-2\": Share2,\n  send: Send,\n  \"external-link\": ExternalLink,\n  \"refresh-cw\": RefreshCw,\n  \"undo-2\": Undo2,\n  archive: Archive,\n  \"trash-2\": Trash2,\n  ban: Ban,\n  lock: Lock,\n  unlock: LockOpen,\n  check: Check,\n  x: X,\n  \"user-plus\": UserPlus,\n  mail: Mail,\n  receipt: Receipt,\n  flag: Flag,\n  star: Star,\n};\n\n/** 12px badge icon. `loader` spins (paused under reduced motion). */\nexport function BadgeIcon({ name }: { name: BadgeIconName }) {\n  const Icon = BADGE[name];\n  return (\n    <Icon\n      size={12}\n      strokeWidth={2.25}\n      aria-hidden=\"true\"\n      focusable=\"false\"\n      className=\"tk-icon tk-badge-icon\"\n      data-spin={name === \"loader\" ? \"\" : undefined}\n    />\n  );\n}\n\nexport function ActionIcon({ name }: { name: ActionIconName }) {\n  const Icon = ACTION[name];\n  return (\n    <Icon size={16} strokeWidth={1.75} aria-hidden=\"true\" focusable=\"false\" className=\"tk-icon\" />\n  );\n}\n"
    },
    {
      "path": "registry/tablekit/index.ts",
      "type": "registry:lib",
      "target": "components/tablekit/index.ts",
      "content": "\"use client\";\nimport \"./tablekit.css\";\nimport { Content } from \"./content\";\nimport { Root } from \"./context\";\nimport { Pagination } from \"./pagination\";\nimport {\n  ColumnToggle,\n  DensityToggle,\n  FilterChips,\n  Filters,\n  Search,\n  SelectionBar,\n  SortSelect,\n  Toolbar,\n} from \"./toolbar\";\n\n/**\n * Composable parts. Use with `useTable`:\n *\n * ```tsx\n * const table = useTable({ data, columns });\n * <Table.Root table={table} aria-label=\"Orders\">\n *   <Table.Toolbar title=\"Orders\"><Table.Search /><Table.Filters /><Table.ColumnToggle /></Table.Toolbar>\n *   <Table.FilterChips />\n *   <Table.SelectionBar>…</Table.SelectionBar>\n *   <Table.Content />\n *   <Table.Pagination />\n * </Table.Root>\n * ```\n */\nexport const Table = {\n  Root,\n  Toolbar,\n  Search,\n  Filters,\n  FilterChips,\n  ColumnToggle,\n  DensityToggle,\n  SortSelect,\n  SelectionBar,\n  Content,\n  Pagination,\n};\n\nexport * from \"./core\";\nexport {\n  type ActionItem,\n  Avatar,\n  Badge,\n  type BadgeProps,\n  CellButton,\n  type CellButtonProps,\n  RowActions,\n  RowActionsGroup,\n  renderSchemaCell,\n  schemaCellText,\n} from \"./cells\";\nexport type { ContentProps } from \"./content\";\nexport {\n  type Density,\n  defaultLabels,\n  type Labels,\n  type Responsive,\n  type RootProps,\n  useTableContext,\n  type Variant,\n} from \"./context\";\nexport { DataTable, type DataTableProps } from \"./DataTable\";\nexport { Popover } from \"./popover\";\nexport type { ColumnToggleProps } from \"./toolbar\";\nexport {\n  type CellContext,\n  type ReactColumnDef,\n  type TableInstance,\n  type UseTableOptions,\n  useTable,\n} from \"./useTable\";\n\nexport { parseTableSchema, toJSONSchema, TableSchema } from \"./core/schema\";\n"
    },
    {
      "path": "registry/tablekit/pagination.tsx",
      "type": "registry:component",
      "target": "components/tablekit/pagination.tsx",
      "content": "\"use client\";\nimport { getPageItems, getPageRange } from \"./core\";\nimport { useId } from \"react\";\nimport { useTableContext } from \"./context\";\nimport { ChevronLeftIcon, ChevronRightIcon } from \"./icons\";\n\nexport function Pagination({\n  pageSizeOptions = [10, 25, 50, 100],\n}: {\n  pageSizeOptions?: number[];\n}) {\n  const { table, labels, announce, layout } = useTableContext();\n  const id = useId();\n  if (table.options.enablePagination === false) return null;\n\n  const { pageCount, totalRows } = table.rowModel;\n  const pageSize = table.state.pagination.pageSize;\n  const pageIndex = Math.min(table.state.pagination.pageIndex, pageCount - 1);\n  const { from, to, total } = getPageRange({ pageIndex, pageSize }, totalRows);\n  const items = getPageItems(pageIndex, pageCount, layout === \"stack\" ? 0 : 1);\n  const sizes = pageSizeOptions.includes(pageSize)\n    ? pageSizeOptions\n    : [...pageSizeOptions, pageSize].sort((a, b) => a - b);\n\n  const go = (i: number) => {\n    table.setPageIndex(i);\n    announce(labels.pageOf(i + 1, pageCount));\n  };\n\n  return (\n    <div className=\"tk-pagination\">\n      <div className=\"tk-page-size\">\n        <label htmlFor={id}>{labels.rowsPerPage}</label>\n        <select\n          id={id}\n          className=\"tk-input tk-select\"\n          value={pageSize}\n          onChange={(e) => table.setPageSize(Number(e.target.value))}\n        >\n          {sizes.map((s) => (\n            <option key={s} value={s}>\n              {s}\n            </option>\n          ))}\n        </select>\n      </div>\n      <p className=\"tk-page-range\">{labels.range(from, to, total)}</p>\n      <nav className=\"tk-pages\" aria-label={labels.pagination}>\n        <button\n          type=\"button\"\n          className=\"tk-icon-button\"\n          aria-label={labels.previousPage}\n          disabled={pageIndex === 0}\n          onClick={() => go(pageIndex - 1)}\n        >\n          <ChevronLeftIcon />\n        </button>\n        <ol className=\"tk-page-list\">\n          {items.map((item, i) =>\n            item === \"…\" ? (\n              // biome-ignore lint/suspicious/noArrayIndexKey: ellipses have no identity\n              <li key={`e${i}`} className=\"tk-page-ellipsis\" aria-hidden=\"true\">\n                …\n              </li>\n            ) : (\n              <li key={item}>\n                <button\n                  type=\"button\"\n                  className=\"tk-page\"\n                  aria-label={labels.page(item)}\n                  aria-current={item === pageIndex + 1 ? \"page\" : undefined}\n                  onClick={() => go(item - 1)}\n                >\n                  {item}\n                </button>\n              </li>\n            ),\n          )}\n        </ol>\n        <button\n          type=\"button\"\n          className=\"tk-icon-button\"\n          aria-label={labels.nextPage}\n          disabled={pageIndex >= pageCount - 1}\n          onClick={() => go(pageIndex + 1)}\n        >\n          <ChevronRightIcon />\n        </button>\n      </nav>\n    </div>\n  );\n}\n"
    },
    {
      "path": "registry/tablekit/popover.tsx",
      "type": "registry:component",
      "target": "components/tablekit/popover.tsx",
      "content": "\"use client\";\nimport {\n  type CSSProperties,\n  type KeyboardEvent,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\n\nexport interface PopoverProps {\n  /** Renders the trigger. Spread `props` onto a <button>. */\n  trigger: (props: {\n    ref: (el: HTMLButtonElement | null) => void;\n    \"aria-expanded\": boolean;\n    \"aria-controls\": string | undefined;\n    \"aria-haspopup\": \"dialog\" | \"menu\";\n    onClick: () => void;\n  }) => ReactNode;\n  children: ReactNode | ((close: () => void) => ReactNode);\n  role?: \"dialog\" | \"menu\";\n  label: string;\n  align?: \"start\" | \"end\";\n  className?: string;\n}\n\n/**\n * Minimal, dependency-free popover: click-outside + Escape to close, focus moves in on open\n * and returns to the trigger on close. `role=\"menu\"` adds arrow-key roving between items.\n *\n * The panel is portaled into the nearest `.tk-root` (not <body>) so it keeps inheriting the\n * table's tokens, theme and preset scope, and is promoted to the browser's top layer with the\n * Popover API, so no ancestor's `overflow: hidden`, transform or containment can clip it.\n * (Without the Popover API it falls back to absolute positioning inside the root.)\n */\nexport function Popover({\n  trigger,\n  children,\n  role = \"dialog\",\n  label,\n  align = \"start\",\n  className,\n}: PopoverProps) {\n  const [open, setOpen] = useState(false);\n  const id = useId();\n  const triggerRef = useRef<HTMLButtonElement | null>(null);\n  const panelRef = useRef<HTMLDivElement | null>(null);\n  const [host, setHost] = useState<HTMLElement | null>(null);\n  const [pos, setPos] = useState<CSSProperties>({ visibility: \"hidden\" });\n\n  const close = useCallback((restoreFocus = true) => {\n    setOpen(false);\n    if (restoreFocus) triggerRef.current?.focus();\n  }, []);\n\n  // `host` is a dependency because the panel only mounts once the host is known.\n  // biome-ignore lint/correctness/useExhaustiveDependencies: see above\n  useEffect(() => {\n    if (!open) return;\n    const panel = panelRef.current;\n    const trig = triggerRef.current;\n    const first = panel?.querySelector<HTMLElement>(\n      role === \"menu\"\n        ? '[role=\"menuitem\"]:not([disabled])'\n        : 'input, select, textarea, button, [href], [tabindex]:not([tabindex=\"-1\"])',\n    );\n    // Wait one frame so the panel is positioned and visible before focusing.\n    const raf = requestAnimationFrame(() => first?.focus({ preventScroll: true }));\n\n    const onPointer = (e: PointerEvent) => {\n      const t = e.target as Node;\n      if (!panel?.contains(t) && !trig?.contains(t)) close(false);\n    };\n    document.addEventListener(\"pointerdown\", onPointer);\n    return () => {\n      cancelAnimationFrame(raf);\n      document.removeEventListener(\"pointerdown\", onPointer);\n    };\n  }, [open, role, close, host]);\n\n  // Place next to the trigger, flipping to stay inside the viewport. In the top layer the\n  // panel is fixed to the viewport; in the fallback it's absolute inside the host.\n  const place = useCallback(() => {\n    const panel = panelRef.current;\n    const trig = triggerRef.current;\n    if (!panel || !trig || !host) return;\n    // Promote to the top layer first (Popover API), so the measurements below are final.\n    const p = panel as HTMLElement & { showPopover?: () => void };\n    let topLayer = false;\n    if (p.hasAttribute(\"popover\")) {\n      try {\n        p.showPopover?.(); // throws if it's already open, which is fine\n      } catch {}\n      try {\n        topLayer = p.matches(\":popover-open\");\n      } catch {}\n      // No working Popover API (older browsers, jsdom): drop the attribute so the UA's\n      // [popover] { display: none } can't hide the panel, and use the absolute fallback.\n      if (!topLayer) p.removeAttribute(\"popover\");\n    } else {\n      topLayer = false;\n    }\n    const t = trig.getBoundingClientRect();\n    const w = panel.offsetWidth;\n    const ph = panel.offsetHeight;\n    const vw = document.documentElement.clientWidth;\n    let side = align;\n    if (side === \"start\" && t.left + w > vw - 8) side = \"end\";\n    else if (side === \"end\" && t.right - w < 8) side = \"start\";\n    const below = t.bottom + 6 + ph <= window.innerHeight - 8 || t.top - 6 - ph < 8;\n    // Viewport coordinates, clamped so the panel never leaves the screen.\n    const left = Math.min(Math.max(side === \"start\" ? t.left : t.right - w, 8), vw - w - 8);\n    const top = below ? t.bottom + 6 : t.top - 6 - ph;\n    if (topLayer) {\n      setPos({ position: \"fixed\", top, left });\n    } else {\n      const h = host.getBoundingClientRect();\n      setPos({ top: top - h.top, left: left - h.left });\n    }\n  }, [align, host]);\n\n  useLayoutEffect(() => {\n    if (!open) return;\n    place();\n    const onMove = () => place();\n    window.addEventListener(\"resize\", onMove);\n    window.addEventListener(\"scroll\", onMove, true);\n    return () => {\n      window.removeEventListener(\"resize\", onMove);\n      window.removeEventListener(\"scroll\", onMove, true);\n    };\n  }, [open, place]);\n\n  const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n    if (e.key === \"Escape\") {\n      e.stopPropagation();\n      close();\n      return;\n    }\n    if (role === \"dialog\" && e.key === \"Tab\") {\n      // Close when tabbing out so it never traps focus.\n      requestAnimationFrame(() => {\n        if (!panelRef.current?.contains(document.activeElement)) setOpen(false);\n      });\n    }\n    if (role !== \"menu\") return;\n    const items = [\n      ...(panelRef.current?.querySelectorAll<HTMLElement>('[role=\"menuitem\"]:not([disabled])') ??\n        []),\n    ];\n    const i = items.indexOf(document.activeElement as HTMLElement);\n    let next = -1;\n    if (e.key === \"ArrowDown\") next = (i + 1) % items.length;\n    else if (e.key === \"ArrowUp\") next = (i - 1 + items.length) % items.length;\n    else if (e.key === \"Home\") next = 0;\n    else if (e.key === \"End\") next = items.length - 1;\n    else if (e.key === \"Tab\") close(false);\n    if (next >= 0) {\n      e.preventDefault();\n      items[next]?.focus();\n    }\n  };\n\n  return (\n    <div className=\"tk-popover-anchor\">\n      {trigger({\n        ref: (el) => {\n          triggerRef.current = el;\n        },\n        \"aria-expanded\": open,\n        \"aria-controls\": open ? id : undefined,\n        \"aria-haspopup\": role,\n        onClick: () => {\n          setHost((triggerRef.current?.closest(\".tk-root\") as HTMLElement | null) ?? document.body);\n          setPos({ visibility: \"hidden\" });\n          setOpen((o) => !o);\n        },\n      })}\n      {open &&\n        host &&\n        createPortal(\n          // biome-ignore lint/a11y/noStaticElementInteractions: role is dialog or menu (dynamic)\n          // biome-ignore lint/a11y/useAriaPropsSupportedByRole: role is dialog or menu (dynamic)\n          <div\n            ref={panelRef}\n            id={id}\n            role={role}\n            aria-label={label}\n            aria-modal={role === \"dialog\" ? false : undefined}\n            // \"manual\": we handle outside-click and Escape ourselves (see above).\n            popover=\"manual\"\n            className={`tk-popover${className ? ` ${className}` : \"\"}`}\n            style={pos}\n            onKeyDown={onKeyDown}\n          >\n            {typeof children === \"function\" ? children(() => close()) : children}\n          </div>,\n          host,\n        )}\n    </div>\n  );\n}\n"
    },
    {
      "path": "registry/tablekit/toolbar.tsx",
      "type": "registry:component",
      "target": "components/tablekit/toolbar.tsx",
      "content": "\"use client\";\nimport {\n  type FilterValue,\n  getFacetValues,\n  isFilterActive,\n  type RangeFilterValue,\n  type RowData,\n} from \"./core\";\nimport {\n  type KeyboardEvent,\n  type PointerEvent,\n  type ReactNode,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { type Density, useTableContext } from \"./context\";\nimport { useDebounced } from \"./hooks\";\nimport {\n  CloseIcon,\n  ColumnsIcon,\n  DensityComfortableIcon,\n  DensityCompactIcon,\n  DensityDefaultIcon,\n  FilterIcon,\n  GripIcon,\n  PinIcon,\n  SearchIcon,\n} from \"./icons\";\nimport { Popover } from \"./popover\";\nimport type { ReactColumnDef } from \"./useTable\";\n\nexport function Toolbar({\n  title,\n  description,\n  children,\n}: {\n  title?: ReactNode;\n  description?: ReactNode;\n  children?: ReactNode;\n}) {\n  return (\n    <div className=\"tk-toolbar\">\n      {(title || description) && (\n        <div className=\"tk-toolbar-heading\">\n          {title && <h2 className=\"tk-title\">{title}</h2>}\n          {description && <p className=\"tk-description\">{description}</p>}\n        </div>\n      )}\n      {children && <div className=\"tk-toolbar-controls\">{children}</div>}\n    </div>\n  );\n}\n\nexport function Search({\n  placeholder,\n  debounce = 150,\n}: {\n  placeholder?: string;\n  debounce?: number;\n}) {\n  const { table, labels, announce } = useTableContext();\n  const [value, setValue] = useState(table.state.globalFilter);\n  const debounced = useDebounced(value, debounce);\n  const first = useRef(true);\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: only react to the debounced text\n  useEffect(() => {\n    if (first.current) {\n      first.current = false;\n      return;\n    }\n    table.setGlobalFilter(debounced);\n  }, [debounced]);\n\n  // Sync when cleared from outside (e.g. \"Clear filters\").\n  // biome-ignore lint/correctness/useExhaustiveDependencies: external reset only\n  useEffect(() => {\n    if (table.state.globalFilter === \"\" && value !== \"\" && debounced === value) setValue(\"\");\n  }, [table.state.globalFilter]);\n\n  const total = table.rowModel.totalRows;\n  const lastAnnounced = useRef<number | null>(null);\n  useEffect(() => {\n    if (!table.state.globalFilter) return;\n    if (lastAnnounced.current !== total) announce(labels.results(total));\n    lastAnnounced.current = total;\n  }, [total, table.state.globalFilter, announce, labels]);\n\n  return (\n    <div className=\"tk-search\">\n      <SearchIcon />\n      <input\n        type=\"search\"\n        className=\"tk-input\"\n        aria-label={labels.search}\n        placeholder={placeholder ?? labels.searchPlaceholder}\n        value={value}\n        onChange={(e) => setValue(e.target.value)}\n        onKeyDown={(e) => {\n          if (e.key === \"Escape\" && value) {\n            e.preventDefault();\n            setValue(\"\");\n          }\n        }}\n      />\n    </div>\n  );\n}\n\nfunction optionLabel<T extends RowData>(column: ReactColumnDef<T>, value: string): string {\n  return column.optionLabel ? column.optionLabel(value) : value;\n}\n\nfunction RangeInputs<T extends RowData>({ column }: { column: ReactColumnDef<T> }) {\n  const { table, labels } = useTableContext<T>();\n  const current = (table.getColumnFilter(column.id) as RangeFilterValue | undefined) ?? {};\n  const type = column.rangeType === \"date\" ? \"date\" : \"number\";\n  const id = useId();\n  const update = (patch: RangeFilterValue) => {\n    const next = { ...current, ...patch };\n    table.setColumnFilter(column.id, next);\n  };\n  return (\n    <div className=\"tk-range\">\n      <label htmlFor={`${id}-min`} className=\"tk-field\">\n        <span>{labels.min}</span>\n        <input\n          id={`${id}-min`}\n          className=\"tk-input\"\n          type={type}\n          inputMode={type === \"number\" ? \"decimal\" : undefined}\n          value={(current.min as string | number | undefined) ?? \"\"}\n          onChange={(e) => update({ min: e.target.value === \"\" ? null : e.target.value })}\n        />\n      </label>\n      <span aria-hidden=\"true\" className=\"tk-range-sep\">\n        –\n      </span>\n      <label htmlFor={`${id}-max`} className=\"tk-field\">\n        <span>{labels.max}</span>\n        <input\n          id={`${id}-max`}\n          className=\"tk-input\"\n          type={type}\n          inputMode={type === \"number\" ? \"decimal\" : undefined}\n          value={(current.max as string | number | undefined) ?? \"\"}\n          onChange={(e) => update({ max: e.target.value === \"\" ? null : e.target.value })}\n        />\n      </label>\n    </div>\n  );\n}\n\nfunction SelectOptions<T extends RowData>({ column }: { column: ReactColumnDef<T> }) {\n  const { table } = useTableContext<T>();\n  const values = useMemo(\n    () => getFacetValues(table.rowModel.allRows, column.id),\n    [table.rowModel.allRows, column.id],\n  );\n  const selected = (table.getColumnFilter(column.id) as string[] | undefined) ?? [];\n  const toggle = (v: string) =>\n    table.setColumnFilter(\n      column.id,\n      selected.includes(v) ? selected.filter((s) => s !== v) : [...selected, v],\n    );\n  return (\n    <div className=\"tk-options\">\n      {values.map((v) => (\n        <label key={v} className=\"tk-option\">\n          <input\n            type=\"checkbox\"\n            className=\"tk-checkbox\"\n            checked={selected.includes(v)}\n            onChange={() => toggle(v)}\n          />\n          <span>{optionLabel(column, v)}</span>\n        </label>\n      ))}\n    </div>\n  );\n}\n\nfunction TextFilterInput<T extends RowData>({ column }: { column: ReactColumnDef<T> }) {\n  const { table } = useTableContext<T>();\n  return (\n    <input\n      className=\"tk-input\"\n      aria-label={column.header ?? column.id}\n      value={(table.getColumnFilter(column.id) as string | undefined) ?? \"\"}\n      onChange={(e) => table.setColumnFilter(column.id, e.target.value)}\n    />\n  );\n}\n\n/** Popover with one section per filterable column. */\nexport function Filters() {\n  const { table, labels } = useTableContext();\n  const filterable = table.columns.filter((c) => c.filter);\n  if (filterable.length === 0) return null;\n  const count = table.state.columnFilters.length;\n\n  return (\n    <Popover\n      label={labels.filters}\n      align=\"end\"\n      className=\"tk-filters-panel\"\n      trigger={(p) => (\n        <button type=\"button\" className=\"tk-button\" data-active={count > 0 || undefined} {...p}>\n          <FilterIcon />\n          <span>{labels.filters}</span>\n          {count > 0 && <span className=\"tk-count\">{count}</span>}\n        </button>\n      )}\n    >\n      <div className=\"tk-popover-body\">\n        {filterable.map((c) => {\n          const name = c.header ?? c.id;\n          return (\n            <fieldset key={c.id} className=\"tk-filter-section\">\n              <legend>{name}</legend>\n              {c.filter === \"select\" && <SelectOptions column={c} />}\n              {c.filter === \"range\" && <RangeInputs column={c} />}\n              {c.filter === \"text\" && <TextFilterInput column={c} />}\n            </fieldset>\n          );\n        })}\n      </div>\n      <div className=\"tk-popover-footer\">\n        <button\n          type=\"button\"\n          className=\"tk-button tk-button-ghost\"\n          disabled={count === 0}\n          onClick={() => table.setState((s) => ({ ...s, columnFilters: [] }))}\n        >\n          {labels.clearAll}\n        </button>\n      </div>\n    </Popover>\n  );\n}\n\nfunction describeFilter<T extends RowData>(\n  column: ReactColumnDef<T>,\n  value: FilterValue,\n  labels: ReturnType<typeof useTableContext>[\"labels\"],\n): string {\n  if (typeof value === \"string\") return `“${value}”`;\n  if (Array.isArray(value)) return value.map((v) => optionLabel(column, v)).join(\", \");\n  const { min, max } = value;\n  if (min != null && min !== \"\" && max != null && max !== \"\") return `${min} – ${max}`;\n  if (min != null && min !== \"\") return `≥ ${min}`;\n  if (max != null && max !== \"\") return `≤ ${max}`;\n  return labels.any;\n}\n\n/** Removable chips for active column filters. Renders nothing when none are active. */\nexport function FilterChips() {\n  const { table, labels } = useTableContext();\n  const active = table.state.columnFilters.filter((f) => isFilterActive(f.value));\n  if (active.length === 0) return null;\n  return (\n    <ul className=\"tk-chips\" aria-label={labels.filters}>\n      {active.map((f) => {\n        const column = table.getColumn(f.id);\n        if (!column) return null;\n        const name = column.header ?? column.id;\n        return (\n          <li key={f.id} className=\"tk-chip\">\n            <span className=\"tk-chip-name\">{name}:</span>\n            <span className=\"tk-chip-value\">{describeFilter(column, f.value, labels)}</span>\n            <button\n              type=\"button\"\n              className=\"tk-icon-button tk-chip-remove\"\n              aria-label={labels.removeFilter(name)}\n              onClick={() => table.setColumnFilter(f.id, undefined)}\n            >\n              <CloseIcon />\n            </button>\n          </li>\n        );\n      })}\n      <li>\n        <button type=\"button\" className=\"tk-link-button\" onClick={() => table.clearFilters()}>\n          {labels.clearFilters}\n        </button>\n      </li>\n    </ul>\n  );\n}\n\nexport interface ColumnToggleProps {\n  /** Show visibility checkboxes. Default `true`; per column, `hideable: false` opts out. */\n  hide?: boolean;\n  /** Show drag handles to reorder columns. Default `true`; per column, `reorderable: false` opts out. */\n  reorder?: boolean;\n  /** Show pin toggles. Default `true`; per column, `pinnable: false` opts out. */\n  pin?: boolean;\n}\n\n/**\n * Column menu: show/hide, pin, and reorder. Pinned columns are listed first in their own group,\n * mirroring the table. Reorder by dragging the handle (pointer or touch) or with the arrow keys\n * on the handle (Alt+Arrow also works from the checkbox). Moves never cross the pinned boundary;\n * pinning or unpinning is how a column changes group.\n */\nexport function ColumnToggle({ hide = true, reorder = true, pin = true }: ColumnToggleProps = {}) {\n  const { table, labels, announce } = useTableContext();\n  const hintId = useId();\n  const groupId = useId();\n  const listRef = useRef<HTMLDivElement>(null);\n  const [dragging, setDragging] = useState<string | null>(null);\n  // Moving a node in the DOM drops its focus; put it back after React reorders the list.\n  const refocus = useRef<{ id: string; part: string } | null>(null);\n  useLayoutEffect(() => {\n    const r = refocus.current;\n    if (!r) return;\n    refocus.current = null;\n    listRef.current\n      ?.querySelector<HTMLElement>(`[data-column-id=\"${CSS.escape(r.id)}\"] [data-part=\"${r.part}\"]`)\n      ?.focus();\n  });\n\n  const canMove = (c: ReactColumnDef<RowData>) => reorder && c.reorderable !== false;\n  const canPin = (c: ReactColumnDef<RowData>) => pin && c.pinnable !== false;\n  const items = table.orderedColumns.filter(\n    (c) => !c.isActions && ((hide && c.hideable !== false) || canMove(c) || canPin(c)),\n  );\n  if (items.length === 0) return null;\n  const visibleCount = table.visibleColumns.length;\n  const pinned = items.filter((c) => c.pinned);\n  const rest = items.filter((c) => !c.pinned);\n  const anyMovable = items.some(canMove);\n  const anyPinnable = items.some(canPin);\n  const name = (c: ReactColumnDef<RowData>) => c.header ?? c.id;\n\n  const announcePosition = (id: string) => {\n    const c = table.getColumn(id);\n    if (!c) return;\n    // Position as the user sees it in the list, computed from the next order.\n    requestAnimationFrame(() => {\n      const ids = [\n        ...(listRef.current?.querySelectorAll<HTMLElement>(\"[data-column-id]\") ?? []),\n      ].map((el) => el.dataset.columnId);\n      announce(labels.columnMoved(name(c), ids.indexOf(id) + 1, ids.length));\n    });\n  };\n\n  const step = (c: ReactColumnDef<RowData>, delta: -1 | 1, part: string) => {\n    const group = c.pinned ? pinned : rest;\n    const target = group[group.findIndex((g) => g.id === c.id) + delta];\n    if (!target || !canMove(target)) return;\n    refocus.current = { id: c.id, part };\n    table.moveColumn(c.id, target.id);\n    announcePosition(c.id);\n  };\n\n  const onMoveKey =\n    (c: ReactColumnDef<RowData>, part: string, needsAlt: boolean) => (e: KeyboardEvent) => {\n      if (!canMove(c) || (needsAlt && !e.altKey)) return;\n      if (e.key !== \"ArrowUp\" && e.key !== \"ArrowDown\") return;\n      e.preventDefault();\n      step(c, e.key === \"ArrowUp\" ? -1 : 1, part);\n    };\n\n  const onPointerDown = (c: ReactColumnDef<RowData>) => (e: PointerEvent<HTMLButtonElement>) => {\n    if (e.button !== 0) return;\n    e.currentTarget.setPointerCapture(e.pointerId);\n    setDragging(c.id);\n  };\n  const onPointerMove = (c: ReactColumnDef<RowData>) => (e: PointerEvent<HTMLButtonElement>) => {\n    if (dragging !== c.id) return;\n    const over = document\n      .elementFromPoint(e.clientX, e.clientY)\n      ?.closest<HTMLElement>(\"[data-column-id]\");\n    const targetId = over?.dataset.columnId;\n    if (!targetId || targetId === c.id || !listRef.current?.contains(over)) return;\n    const target = table.getColumn(targetId);\n    const targetPinned = items.find((i) => i.id === targetId)?.pinned;\n    if (!target || !canMove(target) || targetPinned !== c.pinned) return;\n    table.moveColumn(c.id, targetId);\n  };\n  const onPointerEnd = (c: ReactColumnDef<RowData>) => () => {\n    if (dragging !== c.id) return;\n    setDragging(null);\n    announcePosition(c.id);\n  };\n\n  const togglePin = (c: ReactColumnDef<RowData>) => {\n    const next = !c.pinned;\n    refocus.current = { id: c.id, part: \"pin\" };\n    table.setColumnPinned(c.id, next);\n    announce(next ? labels.columnPinned(name(c)) : labels.columnUnpinned(name(c)));\n  };\n\n  const renderItem = (c: ReactColumnDef<RowData>) => {\n    const visible = table.state.columnVisibility[c.id] !== false;\n    const hideable = hide && c.hideable !== false;\n    return (\n      <li\n        key={c.id}\n        className=\"tk-column-item\"\n        data-column-id={c.id}\n        data-dragging={dragging === c.id || undefined}\n      >\n        {anyMovable &&\n          (canMove(c) ? (\n            <button\n              type=\"button\"\n              className=\"tk-icon-button tk-drag-handle\"\n              data-part=\"handle\"\n              aria-label={labels.moveColumn(name(c))}\n              aria-describedby={hintId}\n              onKeyDown={onMoveKey(c, \"handle\", false)}\n              onPointerDown={onPointerDown(c)}\n              onPointerMove={onPointerMove(c)}\n              onPointerUp={onPointerEnd(c)}\n              onPointerCancel={onPointerEnd(c)}\n            >\n              <GripIcon />\n            </button>\n          ) : (\n            <span className=\"tk-drag-handle\" aria-hidden=\"true\" />\n          ))}\n        {hide ? (\n          <label className=\"tk-option\">\n            <input\n              type=\"checkbox\"\n              className=\"tk-checkbox\"\n              data-part=\"toggle\"\n              checked={visible}\n              // Never allow hiding the last visible column.\n              disabled={!hideable || (visible && visibleCount <= 1)}\n              onChange={() => table.toggleColumnVisibility(c.id)}\n              onKeyDown={onMoveKey(c, \"toggle\", true)}\n            />\n            <span>{name(c)}</span>\n          </label>\n        ) : (\n          <span className=\"tk-option\">{name(c)}</span>\n        )}\n        {anyPinnable && canPin(c) && (\n          <button\n            type=\"button\"\n            className=\"tk-icon-button tk-pin-toggle\"\n            data-part=\"pin\"\n            aria-label={labels.pinColumn(name(c))}\n            aria-pressed={c.pinned === true}\n            onClick={() => togglePin(c)}\n          >\n            <PinIcon />\n          </button>\n        )}\n      </li>\n    );\n  };\n\n  return (\n    <Popover\n      label={anyMovable || anyPinnable ? labels.columnSettings : labels.toggleColumns}\n      align=\"end\"\n      trigger={(p) => (\n        <button type=\"button\" className=\"tk-button\" {...p}>\n          <ColumnsIcon />\n          <span>{labels.columns}</span>\n        </button>\n      )}\n    >\n      <div\n        className=\"tk-popover-body tk-column-menu\"\n        ref={listRef}\n        data-dragging={dragging ? \"\" : undefined}\n      >\n        {pinned.length > 0 && (\n          <>\n            <p className=\"tk-column-group\" id={`${groupId}-pinned`}>\n              {labels.pinnedColumns}\n            </p>\n            <ul className=\"tk-column-list\" aria-labelledby={`${groupId}-pinned`}>\n              {pinned.map(renderItem)}\n            </ul>\n            <hr className=\"tk-menu-separator\" />\n            <p className=\"tk-column-group\" id={`${groupId}-rest`}>\n              {labels.otherColumns}\n            </p>\n          </>\n        )}\n        <ul\n          className=\"tk-column-list\"\n          aria-labelledby={pinned.length > 0 ? `${groupId}-rest` : undefined}\n        >\n          {rest.map(renderItem)}\n        </ul>\n        {anyMovable && (\n          <p id={hintId} className=\"tk-sr-only\">\n            {labels.moveColumnHint}\n          </p>\n        )}\n      </div>\n    </Popover>\n  );\n}\n\nexport function DensityToggle() {\n  const { density, setDensity, labels } = useTableContext();\n  const name = useId();\n  const options: [Density, string][] = [\n    [\"compact\", labels.densityCompact],\n    [\"default\", labels.densityDefault],\n    [\"comfortable\", labels.densityComfortable],\n  ];\n  return (\n    <fieldset className=\"tk-segmented\">\n      <legend className=\"tk-sr-only\">{labels.density}</legend>\n      {options.map(([value, text]) => (\n        <label key={value} className=\"tk-segment\" data-checked={density === value || undefined}>\n          <input\n            type=\"radio\"\n            name={name}\n            value={value}\n            checked={density === value}\n            onChange={() => setDensity(value)}\n            className=\"tk-sr-only\"\n          />\n          <DensityGlyph density={value} />\n          <span className=\"tk-sr-only\">{text}</span>\n        </label>\n      ))}\n    </fieldset>\n  );\n}\n\nfunction DensityGlyph({ density }: { density: Density }) {\n  if (density === \"compact\") return <DensityCompactIcon />;\n  if (density === \"comfortable\") return <DensityComfortableIcon />;\n  return <DensityDefaultIcon />;\n}\n\n/** Sort control for the stacked (card) layout, where there are no column headers. */\nexport function SortSelect() {\n  const { table, labels, layout, announce } = useTableContext();\n  const sortable = table.columns.filter((c) => c.sortable !== false);\n  const id = useId();\n  if (layout !== \"stack\" || sortable.length === 0) return null;\n  const current = table.state.sorting[0];\n  const value = current ? `${current.id}:${current.desc ? \"desc\" : \"asc\"}` : \"\";\n  return (\n    <div className=\"tk-sort-select\">\n      <label htmlFor={id} className=\"tk-sr-only\">\n        {labels.sortBy}\n      </label>\n      <select\n        id={id}\n        className=\"tk-input tk-select\"\n        value={value}\n        onChange={(e) => {\n          const [colId, dir] = e.target.value.split(\":\");\n          if (!colId) {\n            table.setSorting([]);\n            announce(labels.sortCleared);\n            return;\n          }\n          table.setSorting([{ id: colId, desc: dir === \"desc\" }]);\n          const name = table.getColumn(colId)?.header ?? colId;\n          announce(dir === \"desc\" ? labels.sortedDesc(name) : labels.sortedAsc(name));\n        }}\n      >\n        <option value=\"\">{labels.noSort}</option>\n        {sortable.map((c) => (\n          <optgroup key={c.id} label={c.header ?? c.id}>\n            <option value={`${c.id}:asc`}>{`${c.header ?? c.id} ↑`}</option>\n            <option value={`${c.id}:desc`}>{`${c.header ?? c.id} ↓`}</option>\n          </optgroup>\n        ))}\n      </select>\n    </div>\n  );\n}\n\n/** Appears when rows are selected. Put bulk-action buttons in `children`. */\nexport function SelectionBar({ children }: { children?: ReactNode }) {\n  const { table, labels } = useTableContext();\n  const n = table.selectedRows.length;\n  if (n === 0) return null;\n  return (\n    <section className=\"tk-selection-bar\" aria-label={labels.selected(n)}>\n      <span className=\"tk-selection-count\" aria-live=\"polite\">\n        {labels.selected(n)}\n      </span>\n      <div className=\"tk-selection-actions\">{children}</div>\n      <button type=\"button\" className=\"tk-button tk-button-ghost\" onClick={table.clearSelection}>\n        {labels.clearSelection}\n      </button>\n    </section>\n  );\n}\n"
    },
    {
      "path": "registry/tablekit/useTable.ts",
      "type": "registry:lib",
      "target": "components/tablekit/useTable.ts",
      "content": "\"use client\";\nimport {\n  type ColumnDef,\n  createInitialState,\n  type FilterValue,\n  functionalUpdate,\n  getOrderedColumns,\n  getRowModel,\n  getSelectionStatus,\n  getSortDirection,\n  getSortIndex,\n  getVisibleColumns,\n  moveColumn as moveColumnCore,\n  type Row,\n  type RowData,\n  type RowModel,\n  resizeColumn as resizeColumnCore,\n  type SortRule,\n  setColumnFilter as setColumnFilterCore,\n  setColumnPinned as setColumnPinnedCore,\n  type TableOptions,\n  type TableState,\n  toggleColumnVisibility as toggleColumnVisibilityCore,\n  toggleRow,\n  toggleRows,\n  toggleSort as toggleSortCore,\n  type Updater,\n} from \"./core\";\nimport { type ReactNode, useCallback, useMemo, useRef, useState } from \"react\";\n\nexport interface CellContext<T extends RowData> {\n  row: Row<T>;\n  column: ReactColumnDef<T>;\n  value: unknown;\n  table: TableInstance<T>;\n}\n\n/** Core ColumnDef plus React renderers. */\nexport interface ReactColumnDef<T extends RowData = RowData> extends ColumnDef<T> {\n  /** Custom cell renderer. Return a string for plain text. */\n  cell?: (ctx: CellContext<T>) => ReactNode;\n  /** Plain-text version of the cell, used for card labels, titles and screen-reader summaries. */\n  text?: (ctx: CellContext<T>) => string;\n  /** Display label for `select` filter options (e.g. \"in_progress\" → \"In progress\"). */\n  optionLabel?: (value: string) => string;\n  /** Input type for `range` filters. Default `number`. */\n  rangeType?: \"number\" | \"date\";\n  /** Row-actions column: rendered in the card header in stacked layout. */\n  isActions?: boolean;\n}\n\nexport interface UseTableOptions<T extends RowData> extends TableOptions<T> {\n  columns: readonly ReactColumnDef<T>[];\n  /** Uncontrolled starting state. */\n  initialState?: Partial<TableState>;\n  /** Controlled state. Any key you pass here is owned by you; the rest stays internal. */\n  state?: Partial<TableState>;\n  /** Called with the full next state on every change. */\n  onStateChange?: (state: TableState) => void;\n  /** Convenience: called with selected row ids whenever selection changes. */\n  onSelectionChange?: (ids: string[]) => void;\n}\n\nexport interface TableInstance<T extends RowData = RowData> {\n  options: UseTableOptions<T>;\n  state: TableState;\n  rowModel: RowModel<T>;\n  columns: readonly ReactColumnDef<T>[];\n  /** Every column in display order (pinned first), `pinned` resolved from state. */\n  orderedColumns: ReactColumnDef<T>[];\n  visibleColumns: ReactColumnDef<T>[];\n  getColumn(id: string): ReactColumnDef<T> | undefined;\n\n  setState(updater: Updater<TableState>): void;\n  // sorting\n  toggleSort(columnId: string, multi?: boolean): void;\n  setSorting(sorting: SortRule[]): void;\n  getSortDirection(columnId: string): \"asc\" | \"desc\" | false;\n  getSortIndex(columnId: string): number;\n  // filtering\n  setGlobalFilter(value: string): void;\n  setColumnFilter(columnId: string, value: FilterValue | undefined): void;\n  getColumnFilter(columnId: string): FilterValue | undefined;\n  clearFilters(): void;\n  hasActiveFilters: boolean;\n  // pagination\n  setPageIndex(index: number): void;\n  setPageSize(size: number): void;\n  // selection\n  toggleRowSelected(rowId: string, value?: boolean): void;\n  togglePageSelected(value: boolean): void;\n  clearSelection(): void;\n  pageSelection: \"none\" | \"some\" | \"all\";\n  selectedRows: Row<T>[];\n  // columns\n  toggleColumnVisibility(columnId: string, value?: boolean): void;\n  resizeColumn(columnId: string, width: number): void;\n  /** Move a column to where `targetId` is now. Ignored across the pinned boundary. */\n  moveColumn(columnId: string, targetId: string): void;\n  setColumnPinned(columnId: string, pinned: boolean): void;\n}\n\nexport function useTable<T extends RowData>(options: UseTableOptions<T>): TableInstance<T> {\n  const [internal, setInternal] = useState<TableState>(() =>\n    createInitialState(options.initialState),\n  );\n  const state = useMemo<TableState>(\n    () => (options.state ? { ...internal, ...options.state } : internal),\n    [internal, options.state],\n  );\n\n  // Keep latest values for stable callbacks.\n  const latest = useRef({ state, options });\n  latest.current = { state, options };\n\n  const setState = useCallback((updater: Updater<TableState>) => {\n    const { state: prev, options: o } = latest.current;\n    const next = functionalUpdate(updater, prev);\n    latest.current.state = next;\n    setInternal(next);\n    o.onStateChange?.(next);\n    if (o.onSelectionChange && next.rowSelection !== prev.rowSelection) {\n      o.onSelectionChange(Object.keys(next.rowSelection).filter((k) => next.rowSelection[k]));\n    }\n  }, []);\n\n  const { data, columns } = options;\n  // biome-ignore lint/correctness/useExhaustiveDependencies: recompute only on inputs that change rows\n  const rowModel = useMemo(\n    () => getRowModel(options, state),\n    [\n      data,\n      columns,\n      state.sorting,\n      state.globalFilter,\n      state.columnFilters,\n      state.pagination,\n      options.manualSorting,\n      options.manualFiltering,\n      options.manualPagination,\n      options.rowCount,\n      options.enablePagination,\n      options.getRowId,\n    ],\n  );\n\n  const orderedColumns = useMemo(\n    () => getOrderedColumns(columns, state.columnOrder, state.columnPinning) as ReactColumnDef<T>[],\n    [columns, state.columnOrder, state.columnPinning],\n  );\n  const visibleColumns = useMemo(\n    () => getVisibleColumns(orderedColumns, state.columnVisibility) as ReactColumnDef<T>[],\n    [orderedColumns, state.columnVisibility],\n  );\n\n  const selectionMode = options.selectionMode ?? \"none\";\n\n  return useMemo<TableInstance<T>>(() => {\n    const byId = new Map(columns.map((c) => [c.id, c]));\n    const resetPage = (s: TableState): TableState[\"pagination\"] => ({\n      ...s.pagination,\n      pageIndex: 0,\n    });\n    return {\n      options,\n      state,\n      rowModel,\n      columns,\n      orderedColumns,\n      visibleColumns,\n      getColumn: (id) => byId.get(id),\n      setState,\n\n      toggleSort: (id, multi) =>\n        setState((s) => ({\n          ...s,\n          sorting: toggleSortCore(s.sorting, id, multi && options.enableMultiSort !== false),\n        })),\n      setSorting: (sorting) => setState((s) => ({ ...s, sorting })),\n      getSortDirection: (id) => getSortDirection(state.sorting, id),\n      getSortIndex: (id) => (state.sorting.length > 1 ? getSortIndex(state.sorting, id) : 0),\n\n      setGlobalFilter: (value) =>\n        setState((s) => ({ ...s, globalFilter: value, pagination: resetPage(s) })),\n      setColumnFilter: (id, value) =>\n        setState((s) => ({\n          ...s,\n          columnFilters: setColumnFilterCore(s.columnFilters, id, value),\n          pagination: resetPage(s),\n        })),\n      getColumnFilter: (id) => state.columnFilters.find((f) => f.id === id)?.value,\n      clearFilters: () =>\n        setState((s) => ({ ...s, globalFilter: \"\", columnFilters: [], pagination: resetPage(s) })),\n      hasActiveFilters: state.globalFilter.trim() !== \"\" || state.columnFilters.length > 0,\n\n      setPageIndex: (pageIndex) =>\n        setState((s) => ({ ...s, pagination: { ...s.pagination, pageIndex } })),\n      setPageSize: (pageSize) =>\n        setState((s) => ({ ...s, pagination: { pageIndex: 0, pageSize } })),\n\n      toggleRowSelected: (id, value) =>\n        setState((s) => ({\n          ...s,\n          rowSelection: toggleRow(s.rowSelection, id, selectionMode, value),\n        })),\n      togglePageSelected: (value) =>\n        setState((s) => ({ ...s, rowSelection: toggleRows(s.rowSelection, rowModel.rows, value) })),\n      clearSelection: () => setState((s) => ({ ...s, rowSelection: {} })),\n      pageSelection: getSelectionStatus(state.rowSelection, rowModel.rows),\n      selectedRows: rowModel.allRows.filter((r) => state.rowSelection[r.id]),\n\n      toggleColumnVisibility: (id, value) =>\n        setState((s) => ({\n          ...s,\n          columnVisibility: toggleColumnVisibilityCore(s.columnVisibility, id, value),\n        })),\n      resizeColumn: (id, width) => {\n        const column = byId.get(id);\n        if (column)\n          setState((s) => ({\n            ...s,\n            columnSizing: resizeColumnCore(s.columnSizing, column, width),\n          }));\n      },\n      moveColumn: (id, targetId) =>\n        setState((s) => ({\n          ...s,\n          columnOrder: moveColumnCore(columns, s.columnOrder, s.columnPinning, id, targetId),\n        })),\n      setColumnPinned: (id, pinned) =>\n        setState((s) => ({\n          ...s,\n          ...setColumnPinnedCore(columns, s.columnOrder, s.columnPinning, id, pinned),\n        })),\n    };\n  }, [options, state, rowModel, columns, orderedColumns, visibleColumns, setState, selectionMode]);\n}\n"
    },
    {
      "path": "registry/tablekit/tablekit.css",
      "type": "registry:file",
      "target": "components/tablekit/tablekit.css",
      "content": "/* @tablekit/tokens — generated by scripts/build.mjs. Do not edit by hand. */\n/*\n * Everything here uses :where() (zero specificity), so any preset or your own\n * :root { --tk-… } overrides win regardless of import order.\n *\n * Theme switching:\n *   - follows prefers-color-scheme by default\n *   - force with data-tk-theme=\"light|dark\" on any ancestor\n *   - also honors the common conventions data-theme=\"light|dark\" and .light / .dark\n */\n\n:where(:root) {\n  --tk-neutral-0: #ffffff;\n  --tk-neutral-25: #fbfbfc;\n  --tk-neutral-50: #f6f7f9;\n  --tk-neutral-100: #eef0f3;\n  --tk-neutral-200: #e2e5ea;\n  --tk-neutral-300: #cdd2da;\n  --tk-neutral-400: #9aa2b1;\n  --tk-neutral-500: #6b7385;\n  --tk-neutral-600: #4f5667;\n  --tk-neutral-700: #3a4050;\n  --tk-neutral-800: #262a35;\n  --tk-neutral-850: #1d2029;\n  --tk-neutral-900: #16181f;\n  --tk-neutral-950: #0f1116;\n  --tk-blue-50: #f2f5fe;\n  --tk-blue-100: #e6ebfd;\n  --tk-blue-200: #cdd6fb;\n  --tk-blue-400: #7d90ec;\n  --tk-blue-500: #4a67e0;\n  --tk-blue-600: #3451d1;\n  --tk-blue-700: #2a41a8;\n  --tk-blue-900: #1a2766;\n  --tk-blue-950: #141c45;\n  --tk-font-family: 'Inter Variable', InterVariable, Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n  --tk-font-family-mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;\n  --tk-font-feature-numeric: \"tnum\" 1, \"zero\" 1, \"ss01\" 1;\n  --tk-font-size: 14px;\n  --tk-font-size-sm: 12px;\n  --tk-font-weight-regular: 400;\n  --tk-font-weight-medium: 500;\n  --tk-font-weight-semibold: 600;\n  --tk-font-line-height: 1.43;\n  --tk-avatar-ring-width: 2px;\n  --tk-avatar-ring-1: #ffc98b;\n  --tk-avatar-ring-2: #e9d3a0;\n  --tk-avatar-ring-3: #ff8f8f;\n  --tk-avatar-ring-4: #f8c3d9;\n  --tk-avatar-ring-5: #f2c4bd;\n  --tk-avatar-ring-6: #b7d5c4;\n  --tk-avatar-ring-7: #bddbb6;\n  --tk-avatar-ring-8: #ffe3b0;\n  --tk-radius-sm: 4px;\n  --tk-radius-md: 8px;\n  --tk-radius-lg: 12px;\n  --tk-radius-pill: 999px;\n  --tk-space-cell-x: 16px;\n  --tk-space-cell-y-compact: 6px;\n  --tk-space-cell-y-default: 10px;\n  --tk-space-cell-y-comfortable: 14px;\n  --tk-space-cell-x-compact: 12px;\n  --tk-space-toolbar-gap: 8px;\n  --tk-space-touch-target: 44px;\n  --tk-shadow-popover: 0px 8px 24px -4px #0f11161a, 0px 2px 6px 0px #0f11160f;\n  --tk-motion-duration: 140ms;\n  --tk-motion-easing: cubic-bezier(0.2, 0, 0, 1);\n}\n\n:where(:root, [data-tk-theme=\"light\"], [data-theme=\"light\"], .light) {\n  --tk-color-surface: var(--tk-neutral-0);\n  --tk-color-surface-raised: var(--tk-neutral-0);\n  --tk-color-header-bg: var(--tk-neutral-50);\n  --tk-color-row-hover: var(--tk-neutral-25);\n  --tk-color-row-stripe: var(--tk-neutral-25);\n  --tk-color-row-selected: var(--tk-blue-50);\n  --tk-color-text: var(--tk-neutral-900);\n  --tk-color-text-muted: var(--tk-neutral-500);\n  --tk-color-text-header: var(--tk-neutral-600);\n  --tk-color-border: var(--tk-neutral-200);\n  --tk-color-border-strong: var(--tk-neutral-300);\n  --tk-color-accent: var(--tk-blue-600);\n  --tk-color-accent-hover: var(--tk-blue-700);\n  --tk-color-accent-contrast: var(--tk-neutral-0);\n  --tk-color-focus: var(--tk-blue-500);\n  --tk-color-danger: #b42323;\n  --tk-color-overlay: rgba(15, 17, 22, 0.08);\n  --tk-color-skeleton: var(--tk-neutral-100);\n  --tk-color-logo-bg: transparent;\n  --tk-tone-neutral-bg: var(--tk-neutral-100);\n  --tk-tone-neutral-fg: var(--tk-neutral-700);\n  --tk-tone-info-bg: #e3effd;\n  --tk-tone-info-fg: #1d5fae;\n  --tk-tone-success-bg: #e3f5ea;\n  --tk-tone-success-fg: #1c7a45;\n  --tk-tone-warning-bg: #fcf1dc;\n  --tk-tone-warning-fg: #8a5a00;\n  --tk-tone-danger-bg: #fde8e8;\n  --tk-tone-danger-fg: #b42323;\n  --tk-tone-accent-bg: var(--tk-blue-100);\n  --tk-tone-accent-fg: var(--tk-blue-700);\n  --tk-scheme-light-display: block;\n  --tk-scheme-dark-display: none;\n  color-scheme: light;\n}\n\n@media (prefers-color-scheme: dark) {\n  :where(:root:not([data-tk-theme=\"light\"], [data-theme=\"light\"], .light)) {\n    --tk-color-surface: var(--tk-neutral-900);\n    --tk-color-surface-raised: var(--tk-neutral-850);\n    --tk-color-header-bg: #1b1e26;\n    --tk-color-row-hover: #1f222b;\n    --tk-color-row-stripe: #191b23;\n    --tk-color-row-selected: var(--tk-blue-950);\n    --tk-color-text: #eceef2;\n    --tk-color-text-muted: var(--tk-neutral-400);\n    --tk-color-text-header: #c3c8d2;\n    --tk-color-border: #2a2e3a;\n    --tk-color-border-strong: var(--tk-neutral-700);\n    --tk-color-accent: var(--tk-blue-400);\n    --tk-color-accent-hover: var(--tk-blue-200);\n    --tk-color-accent-contrast: var(--tk-neutral-950);\n    --tk-color-focus: var(--tk-blue-400);\n    --tk-color-danger: #f59b9b;\n    --tk-color-overlay: rgba(0, 0, 0, 0.4);\n    --tk-color-skeleton: var(--tk-neutral-800);\n    --tk-color-logo-bg: transparent;\n    --tk-color-logo-fill: var(--tk-neutral-800);\n    --tk-tone-neutral-bg: var(--tk-neutral-800);\n    --tk-tone-neutral-fg: #c3c8d2;\n    --tk-tone-info-bg: #132a45;\n    --tk-tone-info-fg: #8cbcf5;\n    --tk-tone-success-bg: #11321f;\n    --tk-tone-success-fg: #7fd6a1;\n    --tk-tone-warning-bg: #36280b;\n    --tk-tone-warning-fg: #f0c46b;\n    --tk-tone-danger-bg: #3d1717;\n    --tk-tone-danger-fg: #f59b9b;\n    --tk-tone-accent-bg: var(--tk-blue-950);\n    --tk-tone-accent-fg: var(--tk-blue-200);\n    --tk-scheme-light-display: none;\n    --tk-scheme-dark-display: block;\n    color-scheme: dark;\n  }\n}\n\n:where([data-theme=\"dark\"], .dark, [data-tk-theme=\"dark\"]) {\n  --tk-color-surface: var(--tk-neutral-900);\n  --tk-color-surface-raised: var(--tk-neutral-850);\n  --tk-color-header-bg: #1b1e26;\n  --tk-color-row-hover: #1f222b;\n  --tk-color-row-stripe: #191b23;\n  --tk-color-row-selected: var(--tk-blue-950);\n  --tk-color-text: #eceef2;\n  --tk-color-text-muted: var(--tk-neutral-400);\n  --tk-color-text-header: #c3c8d2;\n  --tk-color-border: #2a2e3a;\n  --tk-color-border-strong: var(--tk-neutral-700);\n  --tk-color-accent: var(--tk-blue-400);\n  --tk-color-accent-hover: var(--tk-blue-200);\n  --tk-color-accent-contrast: var(--tk-neutral-950);\n  --tk-color-focus: var(--tk-blue-400);\n  --tk-color-danger: #f59b9b;\n  --tk-color-overlay: rgba(0, 0, 0, 0.4);\n  --tk-color-skeleton: var(--tk-neutral-800);\n  --tk-color-logo-bg: transparent;\n  --tk-color-logo-fill: var(--tk-neutral-800);\n  --tk-tone-neutral-bg: var(--tk-neutral-800);\n  --tk-tone-neutral-fg: #c3c8d2;\n  --tk-tone-info-bg: #132a45;\n  --tk-tone-info-fg: #8cbcf5;\n  --tk-tone-success-bg: #11321f;\n  --tk-tone-success-fg: #7fd6a1;\n  --tk-tone-warning-bg: #36280b;\n  --tk-tone-warning-fg: #f0c46b;\n  --tk-tone-danger-bg: #3d1717;\n  --tk-tone-danger-fg: #f59b9b;\n  --tk-tone-accent-bg: var(--tk-blue-950);\n  --tk-tone-accent-fg: var(--tk-blue-200);\n  --tk-scheme-light-display: none;\n  --tk-scheme-dark-display: block;\n  color-scheme: dark;\n}\n\n/*\n * tablekit × shadcn/ui preset\n * Maps --tk-* tokens to shadcn's CSS variables (Tailwind v4 / shadcn ≥ 2.5 style, where\n * variables hold full colors such as oklch(...)). Dark mode follows shadcn's `.dark` class.\n *\n * Import after tokens.css:\n *   @import \"@tablekit/tokens/tokens.css\";\n *   @import \"@tablekit/tokens/presets/shadcn.css\";\n */\n:root {\n  --tk-font-family: var(\n    --font-sans,\n    \"Inter Variable\",\n    InterVariable,\n    Inter,\n    ui-sans-serif,\n    system-ui,\n    -apple-system,\n    \"Segoe UI\",\n    Roboto,\n    sans-serif\n  );\n  --tk-color-surface: var(--background);\n  --tk-color-surface-raised: var(--popover, var(--background));\n  --tk-color-header-bg: color-mix(in oklab, var(--muted) 60%, var(--background));\n  --tk-color-logo-fill: color-mix(in oklab, var(--foreground) 5%, var(--background));\n  --tk-color-row-hover: color-mix(in oklab, var(--muted) 50%, var(--background));\n  --tk-color-row-stripe: color-mix(in oklab, var(--muted) 35%, var(--background));\n  --tk-color-row-selected: color-mix(in oklab, var(--primary) 9%, var(--background));\n  --tk-color-text: var(--foreground);\n  --tk-color-text-muted: var(--muted-foreground);\n  --tk-color-text-header: var(--muted-foreground);\n  --tk-color-border: var(--border);\n  --tk-color-border-strong: color-mix(in oklab, var(--border) 70%, var(--foreground));\n  --tk-color-accent: var(--primary);\n  --tk-color-accent-hover: color-mix(in oklab, var(--primary) 88%, var(--foreground));\n  --tk-color-accent-contrast: var(--primary-foreground);\n  --tk-color-focus: var(--ring);\n  --tk-color-danger: var(--destructive);\n  --tk-color-skeleton: var(--muted);\n  --tk-radius-md: var(--radius, 8px);\n  --tk-radius-sm: calc(var(--radius, 8px) - 4px);\n  --tk-radius-lg: calc(var(--radius, 8px) + 4px);\n  /* Neutral is derived from foreground/background, not --secondary: many themes colour\n     --secondary (brand pink, coral…), and a neutral badge must always read as grey. */\n  --tk-tone-neutral-bg: color-mix(in oklab, var(--foreground) 7%, var(--background));\n  --tk-tone-neutral-fg: color-mix(in oklab, var(--foreground) 72%, var(--background));\n  --tk-tone-danger-bg: color-mix(in oklab, var(--destructive) 12%, var(--background));\n  --tk-tone-danger-fg: var(--destructive);\n  --tk-tone-accent-bg: color-mix(in oklab, var(--primary) 12%, var(--background));\n  --tk-tone-accent-fg: var(--primary);\n}\n\n/*\n * @tablekit/react — component styles.\n * Every visual decision reads a --tk-* token. Override tokens, not selectors.\n * Selectors are wrapped in :where() so your own class overrides always win.\n */\n\n/* ---- Root & density ----------------------------------------------------- */\n\n:where(.tk-root) {\n  --_cell-y: var(--tk-space-cell-y-default, 10px);\n  --_cell-x: var(--tk-space-cell-x, 16px);\n  --_font: var(--tk-font-size, 14px);\n  --_radius: var(--tk-radius-md, 8px);\n  --_duration: var(--tk-motion-duration, 140ms);\n  --_ease: var(--tk-motion-easing, ease);\n\n  container-type: inline-size;\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  min-width: 0;\n  font-family: var(--tk-font-family);\n  font-size: var(--_font);\n  line-height: var(--tk-font-line-height, 1.43);\n  color: var(--tk-color-text);\n  -webkit-font-smoothing: antialiased;\n}\n\n:where(.tk-root[data-density=\"compact\"]) {\n  --_cell-y: var(--tk-space-cell-y-compact, 6px);\n  --_cell-x: var(--tk-space-cell-x-compact, 12px);\n  --_font: calc(var(--tk-font-size, 14px) - 1px);\n}\n:where(.tk-root[data-density=\"comfortable\"]) {\n  --_cell-y: var(--tk-space-cell-y-comfortable, 14px);\n}\n\n:where(.tk-root) *,\n:where(.tk-root) *::before,\n:where(.tk-root) *::after {\n  box-sizing: border-box;\n}\n\n:where(.tk-sr-only) {\n  position: absolute !important;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0 0 0 0);\n  white-space: nowrap;\n  border: 0;\n}\n\n:where(.tk-icon) {\n  flex: none;\n  width: 16px;\n  height: 16px;\n}\n\n/* ---- Focus ---------------------------------------------------------------- */\n\n:where(.tk-root) :focus-visible {\n  outline: 2px solid var(--tk-color-focus);\n  outline-offset: 2px;\n  border-radius: var(--tk-radius-sm, 4px);\n}\n:where(.tk-td:focus-visible, .tk-th:focus-visible) {\n  outline-offset: -2px;\n  border-radius: 0;\n}\n\n/* ---- Buttons & inputs ----------------------------------------------------- */\n\n:where(.tk-button) {\n  display: inline-flex;\n  align-items: center;\n  gap: 6px;\n  height: 32px;\n  padding: 0 12px;\n  font: inherit;\n  font-size: var(--tk-font-size, 14px);\n  font-weight: var(--tk-font-weight-medium, 500);\n  color: var(--tk-color-text);\n  white-space: nowrap;\n  background: var(--tk-color-surface);\n  border: 1px solid var(--tk-color-border);\n  border-radius: var(--_radius);\n  cursor: pointer;\n  transition:\n    background-color var(--_duration) var(--_ease),\n    border-color var(--_duration) var(--_ease);\n}\n:where(.tk-button:hover:not(:disabled)) {\n  background: var(--tk-color-row-hover);\n  border-color: var(--tk-color-border-strong);\n}\n:where(.tk-button[data-active]) {\n  border-color: var(--tk-color-accent);\n  color: var(--tk-color-accent);\n}\n:where(.tk-button[data-tone=\"danger\"]) {\n  color: var(--tk-color-danger);\n}\n:where(.tk-button:disabled) {\n  opacity: 0.5;\n  cursor: not-allowed;\n}\n:where(.tk-button-ghost) {\n  background: transparent;\n  border-color: transparent;\n}\n\n:where(.tk-icon-button) {\n  display: inline-grid;\n  place-items: center;\n  width: 32px;\n  height: 32px;\n  padding: 0;\n  color: var(--tk-color-text-muted);\n  background: transparent;\n  border: 0;\n  border-radius: var(--_radius);\n  cursor: pointer;\n}\n:where(.tk-icon-button:hover:not(:disabled)) {\n  color: var(--tk-color-text);\n  background: var(--tk-color-row-hover);\n}\n:where(.tk-icon-button:disabled) {\n  opacity: 0.4;\n  cursor: not-allowed;\n}\n\n:where(.tk-link-button) {\n  padding: 0 4px;\n  font: inherit;\n  font-size: var(--tk-font-size-sm, 12px);\n  color: var(--tk-color-accent);\n  background: none;\n  border: 0;\n  cursor: pointer;\n}\n:where(.tk-link-button:hover) {\n  text-decoration: underline;\n}\n\n:where(.tk-count, .tk-selection-count, .tk-cards, .tk-chip-value) {\n  font-variant-numeric: tabular-nums;\n  font-feature-settings: var(--tk-font-feature-numeric, \"tnum\" 1, \"zero\" 1, \"ss01\" 1);\n}\n:where(.tk-count) {\n  display: inline-grid;\n  place-items: center;\n  min-width: 18px;\n  height: 18px;\n  padding: 0 5px;\n  font-size: 11px;\n  font-weight: var(--tk-font-weight-semibold, 600);\n  color: var(--tk-color-accent-contrast);\n  background: var(--tk-color-accent);\n  border-radius: var(--tk-radius-pill, 999px);\n}\n\n:where(.tk-input) {\n  height: 32px;\n  width: 100%;\n  min-width: 0;\n  padding: 0 10px;\n  font: inherit;\n  font-size: var(--tk-font-size, 14px);\n  color: var(--tk-color-text);\n  background: var(--tk-color-surface);\n  border: 1px solid var(--tk-color-border);\n  border-radius: var(--_radius);\n  transition: border-color var(--_duration) var(--_ease);\n}\n:where(.tk-input:hover) {\n  border-color: var(--tk-color-border-strong);\n}\n:where(.tk-input:focus-visible) {\n  outline: 2px solid var(--tk-color-focus);\n  outline-offset: -1px;\n  border-color: transparent;\n}\n:where(.tk-select) {\n  width: auto;\n  padding-right: 28px;\n  appearance: none;\n  background-image:\n    linear-gradient(45deg, transparent 50%, currentColor 50%),\n    linear-gradient(135deg, currentColor 50%, transparent 50%);\n  background-position:\n    calc(100% - 14px) 50%,\n    calc(100% - 10px) 50%;\n  background-size: 4px 4px;\n  background-repeat: no-repeat;\n  cursor: pointer;\n}\n\n:where(.tk-checkbox) {\n  appearance: none;\n  display: inline-grid;\n  place-content: center;\n  flex: none;\n  width: 16px;\n  height: 16px;\n  margin: 0;\n  background: var(--tk-color-surface);\n  border: 1.5px solid var(--tk-color-border-strong);\n  border-radius: var(--tk-radius-sm, 4px);\n  cursor: pointer;\n  transition:\n    background-color var(--_duration) var(--_ease),\n    border-color var(--_duration) var(--_ease);\n}\n:where(.tk-checkbox:hover) {\n  border-color: var(--tk-color-accent);\n}\n:where(.tk-checkbox:checked, .tk-checkbox:indeterminate) {\n  background: var(--tk-color-accent);\n  border-color: var(--tk-color-accent);\n}\n:where(.tk-checkbox)::before {\n  content: \"\";\n  width: 10px;\n  height: 10px;\n  background: var(--tk-color-accent-contrast);\n  transform: scale(0);\n  transition: transform var(--_duration) var(--_ease);\n  clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);\n}\n:where(.tk-checkbox:checked)::before {\n  transform: scale(1);\n}\n:where(.tk-checkbox:indeterminate)::before {\n  transform: scale(1);\n  clip-path: inset(40% 5% 40% 5%);\n}\n:where(.tk-checkbox:disabled) {\n  opacity: 0.5;\n  cursor: not-allowed;\n}\n\n/* ---- Toolbar ---------------------------------------------------------------- */\n\n:where(.tk-toolbar) {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: flex-end;\n  justify-content: space-between;\n  gap: 12px;\n}\n:where(.tk-toolbar-heading) {\n  min-width: 0;\n}\n:where(.tk-title) {\n  margin: 0;\n  font-size: calc(var(--tk-font-size, 14px) + 2px);\n  font-weight: var(--tk-font-weight-semibold, 600);\n  line-height: 1.3;\n}\n:where(.tk-description) {\n  margin: 2px 0 0;\n  font-size: var(--tk-font-size-sm, 12px);\n  color: var(--tk-color-text-muted);\n}\n:where(.tk-toolbar-controls) {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n  gap: var(--tk-space-toolbar-gap, 8px);\n  margin-left: auto;\n}\n\n:where(.tk-search) {\n  position: relative;\n  display: flex;\n  align-items: center;\n  /* Controls are end-aligned, so extra width grows the field to the left. */\n  width: var(--tk-search-width, 320px);\n  max-width: 100%;\n}\n:where(.tk-search) > .tk-icon {\n  position: absolute;\n  left: 10px;\n  color: var(--tk-color-text-muted);\n  pointer-events: none;\n}\n:where(.tk-search .tk-input) {\n  padding-left: 32px;\n}\n\n:where(.tk-segmented) {\n  display: inline-flex;\n  margin: 0;\n  padding: 2px;\n  border: 1px solid var(--tk-color-border);\n  border-radius: var(--_radius);\n  background: var(--tk-color-surface);\n}\n:where(.tk-segment) {\n  display: grid;\n  place-items: center;\n  width: 28px;\n  height: 26px;\n  color: var(--tk-color-text-muted);\n  border-radius: calc(var(--_radius) - 2px);\n  cursor: pointer;\n}\n:where(.tk-segment[data-checked]) {\n  color: var(--tk-color-text);\n  background: var(--tk-color-header-bg);\n}\n:where(.tk-segment:has(:focus-visible)) {\n  outline: 2px solid var(--tk-color-focus);\n}\n\n/* Chips */\n:where(.tk-chips) {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n  gap: 6px;\n  margin: -4px 0 0;\n  padding: 0;\n  list-style: none;\n}\n:where(.tk-chip) {\n  display: inline-flex;\n  align-items: center;\n  gap: 4px;\n  height: 26px;\n  padding: 0 2px 0 10px;\n  font-size: var(--tk-font-size-sm, 12px);\n  background: var(--tk-color-header-bg);\n  border: 1px solid var(--tk-color-border);\n  border-radius: var(--tk-radius-pill, 999px);\n}\n:where(.tk-chip-name) {\n  color: var(--tk-color-text-muted);\n}\n:where(.tk-chip-value) {\n  max-width: 24ch;\n  overflow: hidden;\n  font-weight: var(--tk-font-weight-medium, 500);\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n:where(.tk-chip-remove) {\n  width: 22px;\n  height: 22px;\n  border-radius: var(--tk-radius-pill, 999px);\n}\n:where(.tk-chip-remove .tk-icon) {\n  width: 12px;\n  height: 12px;\n}\n\n/* Selection bar */\n:where(.tk-selection-bar) {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n  gap: 8px;\n  padding: 6px 6px 6px 14px;\n  background: var(--tk-color-row-selected);\n  border: 1px solid color-mix(in oklab, var(--tk-color-accent) 30%, transparent);\n  border-radius: var(--_radius);\n  animation: tk-slide-in var(--_duration) var(--_ease);\n}\n:where(.tk-selection-count) {\n  font-weight: var(--tk-font-weight-semibold, 600);\n  color: var(--tk-color-accent);\n}\n:where(.tk-selection-actions) {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 6px;\n  margin-left: auto;\n}\n\n/* ---- Popover & menu ---------------------------------------------------------- */\n\n:where(.tk-popover-anchor) {\n  position: relative;\n  display: inline-flex;\n}\n:where(.tk-popover) {\n  position: absolute;\n  z-index: 50;\n  /* Reset the UA [popover] box (inset: 0, margin: auto, border, padding, colours). */\n  inset: auto;\n  margin: 0;\n  padding: 0;\n  min-width: 220px;\n  max-width: min(340px, calc(100vw - 32px));\n  max-height: min(420px, 70vh);\n  overflow: auto;\n  color: var(--tk-color-text);\n  background: var(--tk-color-surface-raised);\n  border: 1px solid var(--tk-color-border);\n  border-radius: var(--tk-radius-lg, 12px);\n  box-shadow: var(--tk-shadow-popover);\n  animation: tk-pop-in var(--_duration) var(--_ease);\n}\n:where(.tk-popover-body) {\n  display: grid;\n  gap: 12px;\n  padding: 12px;\n}\n:where(.tk-popover-footer) {\n  position: sticky;\n  bottom: 0;\n  display: flex;\n  justify-content: flex-end;\n  padding: 6px;\n  background: var(--tk-color-surface-raised);\n  border-top: 1px solid var(--tk-color-border);\n}\n:where(.tk-filters-panel) {\n  width: 300px;\n}\n:where(.tk-filter-section) {\n  display: grid;\n  gap: 6px;\n  margin: 0;\n  padding: 0;\n  border: 0;\n}\n:where(.tk-filter-section) > legend {\n  padding: 0;\n  margin-bottom: 6px;\n  font-size: var(--tk-font-size-sm, 12px);\n  font-weight: var(--tk-font-weight-semibold, 600);\n  color: var(--tk-color-text-header);\n}\n:where(.tk-options) {\n  display: grid;\n  gap: 2px;\n}\n:where(.tk-option) {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  min-height: 32px;\n  padding: 4px 8px;\n  margin: 0 -8px;\n  border-radius: var(--tk-radius-sm, 4px);\n  cursor: pointer;\n}\n:where(.tk-option:hover) {\n  background: var(--tk-color-row-hover);\n}\n/* Column menu: [handle] [checkbox + name] [pin] per row; pinned columns in their own group. */\n:where(.tk-column-menu) {\n  gap: 4px;\n  min-width: 232px;\n}\n:where(.tk-column-list) {\n  display: grid;\n  gap: 2px;\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n:where(.tk-column-group) {\n  margin: 4px 0 0;\n  font-size: var(--tk-font-size-sm, 12px);\n  font-weight: var(--tk-font-weight-medium, 500);\n  color: var(--tk-color-text-muted);\n}\n:where(.tk-column-menu > .tk-menu-separator) {\n  margin: 4px -12px;\n}\n:where(.tk-column-item) {\n  display: flex;\n  align-items: center;\n  gap: 2px;\n  margin: 0 -8px 0 -10px;\n  border-radius: var(--tk-radius-sm, 4px);\n}\n:where(.tk-column-item:hover) {\n  background: var(--tk-color-row-hover);\n}\n:where(.tk-column-item .tk-option) {\n  flex: 1;\n  min-width: 0;\n  margin: 0;\n  padding-inline: 4px;\n}\n:where(.tk-column-item .tk-option:hover) {\n  background: transparent;\n}\n:where(.tk-drag-handle) {\n  flex: none;\n  width: 24px;\n  height: 32px;\n  cursor: grab;\n  touch-action: none;\n}\n:where(.tk-column-item[data-dragging]) {\n  background: var(--tk-color-surface-raised, var(--tk-color-surface));\n  box-shadow:\n    0 0 0 1px var(--tk-color-border),\n    0 4px 12px -4px color-mix(in oklab, var(--tk-color-text) 24%, transparent);\n}\n:where(.tk-column-menu[data-dragging]),\n:where(.tk-column-menu[data-dragging] *) {\n  cursor: grabbing;\n  user-select: none;\n}\n:where(.tk-pin-toggle) {\n  flex: none;\n  opacity: 0.55;\n}\n:where(.tk-column-item:hover .tk-pin-toggle, .tk-pin-toggle:focus-visible) {\n  opacity: 1;\n}\n:where(.tk-pin-toggle[aria-pressed=\"true\"]) {\n  color: var(--tk-color-accent);\n  opacity: 1;\n}\n:where(.tk-pin-toggle[aria-pressed=\"true\"] .tk-icon) {\n  fill: currentColor;\n  transform: none;\n}\n/* Unpinned: tilted, loose. Pinned: upright and filled. */\n:where(.tk-pin-toggle .tk-icon) {\n  transform: rotate(45deg);\n  transition: transform 120ms ease;\n}\n:where(.tk-range) {\n  display: flex;\n  align-items: flex-end;\n  gap: 8px;\n}\n:where(.tk-range-sep) {\n  padding-bottom: 6px;\n  color: var(--tk-color-text-muted);\n}\n:where(.tk-field) {\n  display: grid;\n  flex: 1;\n  gap: 4px;\n  font-size: var(--tk-font-size-sm, 12px);\n  color: var(--tk-color-text-muted);\n}\n\n:where(.tk-menu) {\n  min-width: 160px;\n  padding: 4px;\n}\n:where(.tk-menu-item) {\n  display: flex;\n  align-items: center;\n  width: 100%;\n  min-height: 32px;\n  padding: 0 10px;\n  font: inherit;\n  font-size: var(--tk-font-size, 14px);\n  color: var(--tk-color-text);\n  text-align: left;\n  background: none;\n  border: 0;\n  border-radius: var(--tk-radius-sm, 4px);\n  cursor: pointer;\n}\n:where(.tk-menu-item:hover, .tk-menu-item:focus-visible) {\n  background: var(--tk-color-row-hover);\n  outline: none;\n}\n:where(.tk-menu-item[data-tone=\"danger\"]) {\n  color: var(--tk-color-danger);\n}\n:where(.tk-menu-item) {\n  gap: 10px;\n}\n:where(.tk-menu-item .tk-icon) {\n  color: var(--tk-color-text-muted);\n}\n:where(.tk-menu-item[data-tone=\"danger\"] .tk-icon) {\n  color: currentColor;\n}\n:where(.tk-menu-icon-space) {\n  display: inline-block;\n  flex: none;\n  width: 16px;\n}\n:where(.tk-menu-item:disabled) {\n  color: var(--tk-color-text-muted);\n  opacity: 0.55;\n  cursor: not-allowed;\n}\n:where(.tk-menu-item:disabled:hover) {\n  background: none;\n}\n:where(.tk-menu-separator) {\n  height: 1px;\n  margin: 4px -4px;\n  background: var(--tk-color-border);\n  border: 0;\n}\n\n/* Row actions as icon buttons (actionsDisplay: inline). */\n:where(.tk-actions) {\n  display: inline-flex;\n  align-items: center;\n  gap: 2px;\n  vertical-align: middle;\n}\n:where(.tk-actions .tk-icon-button[data-tone=\"danger\"]:hover:not(:disabled)) {\n  color: var(--tk-color-danger);\n}\n\n/* A single button inside a cell (type: button). */\n:where(.tk-cell-button) {\n  display: inline-flex;\n  align-items: center;\n  gap: 6px;\n  height: 28px;\n  padding: 0 10px;\n  font: inherit;\n  font-size: var(--tk-font-size-sm, 12px);\n  font-weight: var(--tk-font-weight-medium, 500);\n  color: var(--tk-color-text);\n  white-space: nowrap;\n  vertical-align: middle;\n  background: transparent;\n  border: 1px solid var(--tk-color-border);\n  border-radius: var(--tk-radius-md, 8px);\n  cursor: pointer;\n  transition:\n    background-color var(--_duration) var(--_ease),\n    border-color var(--_duration) var(--_ease);\n}\n:where(.tk-cell-button .tk-icon) {\n  width: 14px;\n  height: 14px;\n  margin-left: -2px;\n}\n:where(.tk-cell-button:hover:not(:disabled)) {\n  background: var(--tk-color-row-hover);\n  border-color: var(--tk-color-border-strong);\n}\n:where(.tk-cell-button[data-variant=\"primary\"]) {\n  color: var(--tk-color-accent-contrast);\n  background: var(--tk-color-accent);\n  border-color: transparent;\n}\n:where(.tk-cell-button[data-variant=\"primary\"]:hover:not(:disabled)) {\n  background: var(--tk-color-accent-hover);\n  border-color: transparent;\n}\n:where(.tk-cell-button[data-variant=\"ghost\"]) {\n  border-color: transparent;\n}\n:where(.tk-cell-button[data-tone=\"danger\"]:not([data-variant=\"primary\"])) {\n  color: var(--tk-color-danger);\n}\n:where(.tk-cell-button[data-tone=\"danger\"][data-variant=\"primary\"]) {\n  color: #fff;\n  background: var(--tk-color-danger);\n}\n:where(.tk-cell-button:disabled) {\n  opacity: 0.5;\n  cursor: not-allowed;\n}\n\n/* ---- Table ---------------------------------------------------------------------- */\n\n:where(.tk-content) {\n  position: relative;\n}\n:where(.tk-scroll) {\n  position: relative;\n  overflow: auto;\n  background: var(--tk-color-surface);\n  border: 1px solid var(--tk-color-border);\n  border-radius: var(--_radius);\n  overscroll-behavior-x: contain;\n  scrollbar-width: thin;\n}\n:where(.tk-table) {\n  width: 100%;\n  table-layout: fixed;\n  border-collapse: separate;\n  border-spacing: 0;\n  font-variant-numeric: tabular-nums;\n  font-feature-settings: var(--tk-font-feature-numeric, \"tnum\" 1, \"zero\" 1, \"ss01\" 1);\n}\n\n:where(.tk-th, .tk-td) {\n  padding: var(--_cell-y) var(--_cell-x);\n  text-align: start;\n  vertical-align: middle;\n  border-bottom: 1px solid var(--tk-color-border);\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n:where(.tk-tr:last-child > .tk-td) {\n  border-bottom: 0;\n}\n:where(.tk-th[data-align=\"end\"], .tk-td[data-align=\"end\"]) {\n  text-align: end;\n}\n:where(.tk-th[data-align=\"center\"], .tk-td[data-align=\"center\"]) {\n  text-align: center;\n}\n\n:where(.tk-th) {\n  position: relative;\n  height: calc(var(--_cell-y) * 2 + 20px);\n  font-size: var(--tk-font-size-sm, 12px);\n  font-weight: var(--tk-font-weight-semibold, 600);\n  letter-spacing: 0.01em;\n  color: var(--tk-color-text-header);\n  background: var(--tk-color-header-bg);\n  user-select: none;\n}\n:where(.tk-table[data-sticky-header] thead .tk-th) {\n  position: sticky;\n  top: 0;\n  z-index: 2;\n}\n\n:where(.tk-sort) {\n  display: inline-flex;\n  align-items: center;\n  gap: 4px;\n  max-width: 100%;\n  margin: -4px -6px;\n  padding: 4px 6px;\n  font: inherit;\n  color: inherit;\n  letter-spacing: inherit;\n  background: none;\n  border: 0;\n  border-radius: var(--tk-radius-sm, 4px);\n  cursor: pointer;\n}\n:where(.tk-th[data-align=\"end\"] .tk-sort) {\n  flex-direction: row-reverse;\n}\n:where(.tk-sort:hover) {\n  color: var(--tk-color-text);\n  background: var(--tk-color-overlay);\n}\n:where(.tk-th-label) {\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n:where(.tk-sort-icon) {\n  display: inline-flex;\n  color: var(--tk-color-text-muted);\n  opacity: 0.45;\n  transition: opacity var(--_duration) var(--_ease);\n}\n:where(.tk-sort:hover .tk-sort-icon) {\n  opacity: 0.8;\n}\n:where(.tk-th[data-sorted]) {\n  color: var(--tk-color-text);\n}\n:where(.tk-th[data-sorted] .tk-sort-icon) {\n  color: var(--tk-color-accent);\n  opacity: 1;\n}\n:where(.tk-sort-icon .tk-icon) {\n  width: 14px;\n  height: 14px;\n}\n:where(.tk-sort-index) {\n  font-size: 10px;\n  color: var(--tk-color-accent);\n}\n\n:where(.tk-resize) {\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  width: 9px;\n  cursor: col-resize;\n  touch-action: none;\n}\n:where(.tk-resize)::after {\n  content: \"\";\n  position: absolute;\n  top: 25%;\n  bottom: 25%;\n  left: 4px;\n  width: 1px;\n  background: var(--tk-color-border);\n  transition:\n    background-color var(--_duration) var(--_ease),\n    top var(--_duration),\n    bottom var(--_duration);\n}\n:where(.tk-resize:hover, .tk-resize:focus-visible, .tk-resize:active)::after {\n  top: 0;\n  bottom: 0;\n  width: 2px;\n  left: 3.5px;\n  background: var(--tk-color-accent);\n}\n:where(.tk-resize:focus-visible) {\n  outline: none;\n}\n\n:where(.tk-td) {\n  background: var(--tk-color-surface);\n  transition: background-color var(--_duration) var(--_ease);\n}\n:where(.tk-td:focus) {\n  outline: none;\n}\n:where(.tk-td:focus-visible) {\n  outline: 2px solid var(--tk-color-focus);\n}\n:where(.tk-root[data-variant=\"zebra\"] tbody .tk-tr:nth-child(even) > .tk-td) {\n  background: var(--tk-color-row-stripe);\n}\n:where(\n  .tk-root[data-variant=\"bordered\"] .tk-th:not(:last-child),\n  .tk-root[data-variant=\"bordered\"] .tk-td:not(:last-child)\n) {\n  border-right: 1px solid var(--tk-color-border);\n}\n:where(tbody .tk-tr:hover > .tk-td) {\n  background: var(--tk-color-row-hover);\n}\n:where(.tk-tr[data-selected] > .tk-td) {\n  background: var(--tk-color-row-selected);\n}\n:where(.tk-tr[data-clickable]) {\n  cursor: pointer;\n}\n\n:where(.tk-th[data-actions], .tk-td[data-actions]) {\n  padding-inline: 8px;\n  text-overflow: clip;\n}\n:where(.tk-select-cell) {\n  width: 44px;\n  padding-right: 0;\n  padding-left: 14px;\n  text-align: start;\n}\n:where(.tk-select-cell .tk-checkbox) {\n  vertical-align: middle;\n}\n\n/* Pinned columns: opaque background + edge shadow while scrolled. */\n:where(.tk-th[data-pinned], .tk-td[data-pinned]) {\n  position: sticky;\n  z-index: 1;\n}\n/*\n * Stacking while scrolling (all selectors are zero-specificity, so ORDER matters —\n * keep this block after the sticky-header and pinned rules above):\n *   body cells (auto) < pinned body cells (1) < sticky header (2) < pinned header (3)\n */\n:where(thead .tk-th[data-pinned]) {\n  z-index: 3;\n}\n:where(.tk-td[data-pinned]:not(.tk-select-cell), .tk-th[data-pinned]:not(.tk-select-cell)) {\n  box-shadow: inset -1px 0 0 var(--tk-color-border);\n}\n\n/* States */\n:where(.tk-state-row > .tk-td) {\n  white-space: normal;\n}\n:where(.tk-state-row:hover > .tk-td) {\n  background: var(--tk-color-surface);\n}\n:where(.tk-state) {\n  display: grid;\n  justify-items: center;\n  gap: 4px;\n  padding: 48px 16px;\n  text-align: center;\n}\n:where(.tk-state-icon) {\n  display: grid;\n  place-items: center;\n  width: 40px;\n  height: 40px;\n  margin-bottom: 8px;\n  color: var(--tk-color-text-muted);\n  background: var(--tk-color-header-bg);\n  border: 1px solid var(--tk-color-border);\n  border-radius: var(--tk-radius-lg, 12px);\n}\n:where(.tk-state[role=\"alert\"] .tk-state-icon) {\n  color: var(--tk-tone-danger-fg);\n  background: var(--tk-tone-danger-bg);\n  border-color: transparent;\n}\n:where(.tk-state-icon .tk-icon) {\n  width: 20px;\n  height: 20px;\n}\n:where(.tk-state-title) {\n  margin: 0;\n  font-weight: var(--tk-font-weight-semibold, 600);\n}\n:where(.tk-state-description) {\n  max-width: 44ch;\n  margin: 0 0 12px;\n  font-size: var(--tk-font-size-sm, 12px);\n  color: var(--tk-color-text-muted);\n}\n\n/* Loading */\n:where(.tk-skeleton) {\n  display: block;\n  height: 10px;\n  max-width: 100%;\n  background: var(--tk-color-skeleton);\n  border-radius: var(--tk-radius-pill, 999px);\n  animation: tk-pulse 1.4s ease-in-out infinite;\n}\n:where(.tk-skeleton-row:hover > .tk-td) {\n  background: var(--tk-color-surface);\n}\n:where(.tk-progress) {\n  position: absolute;\n  top: 1px;\n  left: 1px;\n  right: 1px;\n  z-index: 5;\n  height: 2px;\n  overflow: hidden;\n  border-radius: var(--_radius) var(--_radius) 0 0;\n}\n:where(.tk-progress)::after {\n  content: \"\";\n  position: absolute;\n  inset: 0;\n  width: 40%;\n  background: var(--tk-color-accent);\n  animation: tk-progress 1.1s var(--_ease) infinite;\n}\n:where(.tk-content[data-loading] tbody) {\n  opacity: 0.7;\n  transition: opacity var(--_duration);\n}\n\n/* ---- Cells --------------------------------------------------------------------------- */\n\n:where(.tk-badge) {\n  /* Each tone sets --_fg / --_bg; fill and stroke are drawn from them. */\n  --_fg: var(--tk-tone-neutral-fg);\n  --_bg: var(--tk-tone-neutral-bg);\n  display: inline-flex;\n  align-items: center;\n  gap: 6px;\n  height: 22px;\n  padding: 0 8px;\n  font-size: var(--tk-font-size-sm, 12px);\n  font-weight: var(--tk-font-weight-medium, 500);\n  color: var(--_fg);\n  background: var(--_bg);\n  border-radius: var(--tk-radius-pill, 999px);\n  white-space: nowrap;\n}\n/* Stroke: an inset 1px ring (no layout shift), tinted from the tone's text colour. */\n:where(.tk-badge[data-stroke]) {\n  box-shadow: inset 0 0 0 1px\n    color-mix(in oklab, var(--_fg) var(--tk-badge-stroke-strength, 32%), transparent);\n}\n/* No fill: transparent pill. With a stroke it reads as an outline badge; without, as tinted text. */\n:where(.tk-badge[data-fill=\"false\"]) {\n  background: transparent;\n}\n:where(.tk-badge[data-fill=\"false\"][data-stroke]) {\n  box-shadow: inset 0 0 0 1px\n    color-mix(in oklab, var(--_fg) var(--tk-badge-outline-strength, 48%), transparent);\n}\n:where(.tk-badge[data-fill=\"false\"]:not([data-stroke])) {\n  padding-inline: 2px;\n}\n:where(.tk-badge-icon) {\n  width: 12px;\n  height: 12px;\n  margin-left: -1px;\n}\n:where(.tk-badge-icon[data-spin]) {\n  animation: tk-spin 1.1s linear infinite;\n}\n/* Icon-only: a round chip; the label is the tooltip and screen-reader text. */\n:where(.tk-badge[data-indicator=\"icon-only\"]) {\n  justify-content: center;\n  width: 22px;\n  padding: 0;\n}\n:where(.tk-badge[data-indicator=\"icon-only\"] .tk-badge-icon) {\n  margin: 0;\n}\n:where(.tk-badge[data-indicator=\"none\"]) {\n  padding: 0 9px;\n}\n:where(.tk-badge-dot) {\n  width: 6px;\n  height: 6px;\n  background: currentColor;\n  border-radius: 50%;\n}\n:where(.tk-badge[data-tone=\"info\"]) {\n  --_fg: var(--tk-tone-info-fg);\n  --_bg: var(--tk-tone-info-bg);\n}\n:where(.tk-badge[data-tone=\"success\"]) {\n  --_fg: var(--tk-tone-success-fg);\n  --_bg: var(--tk-tone-success-bg);\n}\n:where(.tk-badge[data-tone=\"warning\"]) {\n  --_fg: var(--tk-tone-warning-fg);\n  --_bg: var(--tk-tone-warning-bg);\n}\n:where(.tk-badge[data-tone=\"danger\"]) {\n  --_fg: var(--tk-tone-danger-fg);\n  --_bg: var(--tk-tone-danger-bg);\n}\n:where(.tk-badge[data-tone=\"accent\"]) {\n  --_fg: var(--tk-tone-accent-fg);\n  --_bg: var(--tk-tone-accent-bg);\n}\n\n:where(.tk-avatar) {\n  display: inline-flex;\n  align-items: center;\n  gap: 10px;\n  min-width: 0;\n  max-width: 100%;\n  vertical-align: middle;\n}\n:where(.tk-avatar-img) {\n  display: grid;\n  flex: none;\n  place-items: center;\n  box-sizing: border-box;\n  width: 28px;\n  height: 28px;\n  overflow: hidden;\n  font-size: 11px;\n  font-weight: var(--tk-font-weight-semibold, 600);\n  color: var(--_fg, var(--tk-tone-accent-fg));\n  background: var(--_bg, var(--tk-tone-accent-bg));\n  border-radius: 50%;\n}\n/* Initials: one theme tone per person (picked by name in cells.tsx). */\n:where(.tk-avatar-img[data-tone=\"accent\"]) {\n  --_fg: var(--tk-tone-accent-fg);\n  --_bg: var(--tk-tone-accent-bg);\n}\n:where(.tk-avatar-img[data-tone=\"info\"]) {\n  --_fg: var(--tk-tone-info-fg);\n  --_bg: var(--tk-tone-info-bg);\n}\n:where(.tk-avatar-img[data-tone=\"success\"]) {\n  --_fg: var(--tk-tone-success-fg);\n  --_bg: var(--tk-tone-success-bg);\n}\n:where(.tk-avatar-img[data-tone=\"warning\"]) {\n  --_fg: var(--tk-tone-warning-fg);\n  --_bg: var(--tk-tone-warning-bg);\n}\n:where(.tk-avatar-img[data-tone=\"neutral\"]) {\n  --_fg: var(--tk-tone-neutral-fg);\n  --_bg: var(--tk-tone-neutral-bg);\n}\n:where(.tk-root[data-density=\"compact\"] .tk-avatar-img) {\n  width: 22px;\n  height: 22px;\n  font-size: 10px;\n}\n:where(.tk-avatar-img[data-has-image]) {\n  --_ring: var(--tk-avatar-ring-1);\n  /* Behind photos and illustrations; derived from the theme so it flips with dark mode. */\n  background: var(\n    --tk-avatar-image-bg,\n    color-mix(in oklab, var(--tk-color-text) 4%, var(--tk-color-surface))\n  );\n  border: var(--tk-avatar-ring-width, 2px) solid var(--_ring);\n}\n:where(.tk-avatar-img[data-ring=\"2\"]) {\n  --_ring: var(--tk-avatar-ring-2);\n}\n:where(.tk-avatar-img[data-ring=\"3\"]) {\n  --_ring: var(--tk-avatar-ring-3);\n}\n:where(.tk-avatar-img[data-ring=\"4\"]) {\n  --_ring: var(--tk-avatar-ring-4);\n}\n:where(.tk-avatar-img[data-ring=\"5\"]) {\n  --_ring: var(--tk-avatar-ring-5);\n}\n:where(.tk-avatar-img[data-ring=\"6\"]) {\n  --_ring: var(--tk-avatar-ring-6);\n}\n:where(.tk-avatar-img[data-ring=\"7\"]) {\n  --_ring: var(--tk-avatar-ring-7);\n}\n:where(.tk-avatar-img[data-ring=\"8\"]) {\n  --_ring: var(--tk-avatar-ring-8);\n}\n:where(.tk-avatar-img[data-has-image]) {\n  /* Line-art illustrations need a little more room than initials. */\n  width: 32px;\n  height: 32px;\n}\n:where(.tk-root[data-density=\"compact\"] .tk-avatar-img[data-has-image]) {\n  width: 26px;\n  height: 26px;\n  border-width: calc(var(--tk-avatar-ring-width, 2px) * 0.75);\n}\n:where(.tk-root[data-density=\"comfortable\"] .tk-avatar-img[data-has-image]) {\n  width: 36px;\n  height: 36px;\n}\n/* ---- Logo avatars (avatar.logo) ------------------------------------------------ */\n/* inline: a 16px mark, no container, centred on the name's first line (not the middle of\n   name + subtitle). */\n:where(.tk-avatar[data-logo=\"inline\"]) {\n  align-items: flex-start;\n  gap: 8px;\n}\n:where(.tk-avatar-img[data-logo=\"inline\"]) {\n  --_line: calc(var(--tk-font-size, 14px) * 1.25);\n  --_size: var(--tk-avatar-logo-inline-size, 16px);\n  width: var(--_size);\n  height: var(--_size);\n  /* Centre within the first line box; never negative if the mark is taller than the line. */\n  margin-block: max(0px, calc((var(--_line) - var(--_size)) / 2));\n  padding: 0;\n  overflow: visible;\n  font-size: 9px;\n  background: transparent;\n  border: 0;\n  border-radius: var(--tk-radius-sm, 4px);\n}\n:where(.tk-root[data-density=\"compact\"] .tk-avatar-img[data-logo=\"inline\"]) {\n  --_line: calc((var(--tk-font-size, 14px) - 1px) * 1.25);\n}\n/* Optional backing behind inline marks (--tk-color-logo-bg); transparent by default. */\n:where(.tk-avatar-img[data-logo=\"inline\"][data-has-image]) {\n  background: var(--tk-avatar-logo-bg, var(--tk-color-logo-bg, transparent));\n  box-shadow: 0 0 0 1.5px var(--tk-avatar-logo-bg, var(--tk-color-logo-bg, transparent));\n}\n/* circle: the mark centred in a filled circle, no stroke. */\n:where(.tk-avatar-img[data-logo=\"circle\"]) {\n  /* A whisper of the text colour on the surface, so it follows the palette (warm or cool) and\n     the scheme of the active design system; presets map --tk-color-logo-fill to their own\n     surfaces. Marks too dark for a dark fill: pass a light variant via avatar.imageDarkField. */\n  --_fill: var(\n    --tk-avatar-logo-fill,\n    var(--tk-color-logo-fill, color-mix(in oklab, var(--tk-color-text) 4%, var(--tk-color-surface)))\n  );\n  padding: 7px;\n  color: var(--tk-color-text-muted);\n  background: var(--_fill);\n  border: 0;\n  border-radius: 50%;\n}\n:where(.tk-avatar-img[data-logo=\"circle\"][data-logo-fill=\"accent\"]) {\n  --_fill: var(\n    --tk-avatar-logo-fill-accent,\n    color-mix(\n      in oklab,\n      var(--tk-color-accent) 8%,\n      var(--tk-color-logo-fill, var(--tk-color-surface))\n    )\n  );\n}\n:where(.tk-root[data-density=\"compact\"] .tk-avatar-img[data-logo=\"circle\"]) {\n  padding: 5px;\n}\n:where(.tk-avatar-img[data-logo] img) {\n  object-fit: contain;\n  transform: none;\n}\n:where(.tk-avatar-img img) {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  /* Illustrations are drawn on a square with a faint frame; crop it into the circle. */\n  transform: scale(1.12);\n}\n/* avatar.imageDarkField: both images load; the scheme tokens pick one (light when unset). */\n:where(.tk-avatar-img img[data-scheme=\"light\"]) {\n  display: var(--tk-scheme-light-display, block);\n}\n:where(.tk-avatar-img img[data-scheme=\"dark\"]) {\n  display: var(--tk-scheme-dark-display, none);\n}\n:where(.tk-avatar-text) {\n  display: grid;\n  min-width: 0;\n  line-height: 1.25;\n}\n:where(.tk-avatar-name, .tk-avatar-subtitle) {\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n:where(.tk-avatar-name) {\n  font-weight: var(--tk-font-weight-medium, 500);\n}\n:where(.tk-avatar-subtitle) {\n  font-size: var(--tk-font-size-sm, 12px);\n  font-weight: var(--tk-font-weight-regular, 400);\n  color: var(--tk-color-text-muted);\n}\n\n:where(.tk-link) {\n  display: inline-flex;\n  align-items: center;\n  gap: 4px;\n  font-weight: var(--tk-font-weight-medium, 500);\n  color: var(--tk-color-accent);\n  text-decoration: none;\n  text-underline-offset: 2px;\n}\n:where(.tk-link:hover) {\n  text-decoration: underline;\n}\n:where(.tk-boolean) {\n  display: inline-flex;\n  vertical-align: middle;\n}\n:where(.tk-boolean[data-value=\"true\"]) {\n  color: var(--tk-tone-success-fg);\n}\n:where(.tk-boolean[data-value=\"false\"]),\n:where(.tk-empty-value) {\n  color: var(--tk-color-text-muted);\n}\n:where(.tk-num) {\n  font-variant-numeric: tabular-nums;\n  font-feature-settings: var(--tk-font-feature-numeric, \"tnum\" 1, \"zero\" 1, \"ss01\" 1);\n}\n\n/* ---- Pagination ------------------------------------------------------------------------ */\n\n:where(.tk-pagination) {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n  gap: 8px 16px;\n  font-size: var(--tk-font-size-sm, 12px);\n  color: var(--tk-color-text-muted);\n}\n:where(.tk-page-size) {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n:where(.tk-page-size .tk-select) {\n  height: 28px;\n  font-size: var(--tk-font-size-sm, 12px);\n}\n:where(.tk-page-range) {\n  margin: 0;\n  font-variant-numeric: tabular-nums;\n  font-feature-settings: var(--tk-font-feature-numeric, \"tnum\" 1, \"zero\" 1, \"ss01\" 1);\n}\n:where(.tk-pages) {\n  display: flex;\n  align-items: center;\n  gap: 2px;\n  margin-left: auto;\n}\n:where(.tk-page-list) {\n  display: flex;\n  gap: 2px;\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n:where(.tk-page) {\n  min-width: 32px;\n  height: 32px;\n  padding: 0 6px;\n  font: inherit;\n  font-size: var(--tk-font-size-sm, 12px);\n  font-variant-numeric: tabular-nums;\n  font-feature-settings: var(--tk-font-feature-numeric, \"tnum\" 1, \"zero\" 1, \"ss01\" 1);\n  color: var(--tk-color-text);\n  background: none;\n  border: 0;\n  border-radius: var(--_radius);\n  cursor: pointer;\n}\n:where(.tk-page:hover) {\n  background: var(--tk-color-row-hover);\n}\n:where(.tk-page[aria-current=\"page\"]) {\n  font-weight: var(--tk-font-weight-semibold, 600);\n  color: var(--tk-color-accent-contrast);\n  background: var(--tk-color-accent);\n}\n:where(.tk-page-ellipsis) {\n  display: grid;\n  place-items: center;\n  width: 24px;\n}\n\n/* ---- Stacked (card) layout ---------------------------------------------------------------- */\n\n:where(.tk-cards) {\n  display: grid;\n  gap: 8px;\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n:where(.tk-card) {\n  display: grid;\n  gap: 10px;\n  padding: 12px 14px;\n  background: var(--tk-color-surface);\n  border: 1px solid var(--tk-color-border);\n  border-radius: var(--_radius);\n  transition:\n    background-color var(--_duration) var(--_ease),\n    border-color var(--_duration) var(--_ease);\n}\n:where(.tk-card[data-selected]) {\n  background: var(--tk-color-row-selected);\n  border-color: color-mix(in oklab, var(--tk-color-accent) 35%, transparent);\n}\n:where(.tk-card-head) {\n  display: flex;\n  align-items: center;\n  gap: 12px;\n  min-height: 28px;\n}\n:where(.tk-card-title) {\n  flex: 1;\n  min-width: 0;\n  font-weight: var(--tk-font-weight-semibold, 600);\n}\n:where(.tk-card-open) {\n  padding: 0;\n  font: inherit;\n  color: inherit;\n  text-align: start;\n  background: none;\n  border: 0;\n  cursor: pointer;\n}\n:where(.tk-card-open)::after {\n  /* Whole card is the tap target, but checkbox & actions stay above it. */\n  content: \"\";\n  position: absolute;\n  inset: 0;\n}\n:where(.tk-card[data-clickable]) {\n  position: relative;\n}\n:where(\n  .tk-card[data-clickable] .tk-checkbox,\n  .tk-card[data-clickable] .tk-card-actions,\n  .tk-card[data-clickable] a\n) {\n  position: relative;\n  z-index: 1;\n}\n:where(.tk-card-actions) {\n  margin: -4px -6px -4px 0;\n}\n:where(.tk-card-fields) {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(min(120px, 100%), 1fr));\n  gap: 10px 16px;\n  margin: 0;\n}\n:where(.tk-card-field) {\n  display: grid;\n  gap: 2px;\n  min-width: 0;\n}\n:where(.tk-card-field dt) {\n  font-size: var(--tk-font-size-sm, 12px);\n  color: var(--tk-color-text-muted);\n}\n:where(.tk-card-field dd) {\n  margin: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n:where(.tk-cards-state) {\n  background: var(--tk-color-surface);\n  border: 1px solid var(--tk-color-border);\n  border-radius: var(--_radius);\n}\n:where(.tk-card .tk-skeleton) {\n  height: 12px;\n}\n\n/* Narrow containers: tighter toolbar, full-width search, simpler pagination. */\n@container (max-width: 640px) {\n  :where(.tk-toolbar-controls) {\n    width: 100%;\n    margin-left: 0;\n  }\n  :where(.tk-search) {\n    flex: 1 1 100%;\n    width: auto;\n  }\n  :where(.tk-page-size label) {\n    position: absolute;\n    width: 1px;\n    height: 1px;\n    overflow: hidden;\n    clip: rect(0 0 0 0);\n  }\n  :where(.tk-pagination) {\n    gap: 8px;\n  }\n}\n\n/* Touch devices: 44px minimum targets (WCAG 2.5.8 AAA-friendly). */\n@media (pointer: coarse) {\n  :where(.tk-button, .tk-input, .tk-icon-button, .tk-page) {\n    min-height: var(--tk-space-touch-target, 44px);\n  }\n  :where(.tk-icon-button, .tk-page) {\n    min-width: var(--tk-space-touch-target, 44px);\n  }\n  :where(.tk-checkbox) {\n    width: 20px;\n    height: 20px;\n  }\n  :where(.tk-option, .tk-menu-item) {\n    min-height: var(--tk-space-touch-target, 44px);\n  }\n  :where(.tk-drag-handle, .tk-column-item .tk-icon-button) {\n    width: var(--tk-space-touch-target, 44px);\n    height: var(--tk-space-touch-target, 44px);\n  }\n  :where(.tk-resize) {\n    width: 20px;\n  }\n}\n\n/* ---- Motion ------------------------------------------------------------------------------ */\n\n@keyframes tk-spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n@keyframes tk-pulse {\n  50% {\n    opacity: 0.45;\n  }\n}\n@keyframes tk-progress {\n  from {\n    transform: translateX(-100%);\n  }\n  to {\n    transform: translateX(250%);\n  }\n}\n@keyframes tk-pop-in {\n  from {\n    opacity: 0;\n    transform: translateY(-4px) scale(0.98);\n  }\n}\n@keyframes tk-slide-in {\n  from {\n    opacity: 0;\n    transform: translateY(-4px);\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  :where(.tk-root) *,\n  :where(.tk-root) *::before,\n  :where(.tk-root) *::after {\n    animation-duration: 0.01ms !important;\n    animation-iteration-count: 1 !important;\n    transition-duration: 0.01ms !important;\n  }\n}\n\n@media (forced-colors: active) {\n  :where(.tk-checkbox:checked)::before {\n    background: CanvasText;\n  }\n  :where(.tk-tr[data-selected] > .tk-td) {\n    outline: 2px solid Highlight;\n    outline-offset: -2px;\n  }\n  :where(.tk-badge) {\n    border: 1px solid CanvasText;\n  }\n}\n"
    },
    {
      "path": "registry/tablekit/AGENTS.md",
      "type": "registry:file",
      "target": "components/tablekit/AGENTS.md",
      "content": "# tablekit — rules for AI coding agents\n\nYou are using **tablekit**, an accessible, token-driven React table. Follow these rules.\n\nFull docs for agents: https://tablekit.amitpatjoshi.com/llms-full.txt\n\n## Default: schema mode\n\n```tsx\nimport { DataTable } from \"@/components/tablekit\";\nimport \"@/components/tablekit/styles.css\"; // once, in the root layout\n\n<DataTable\n  data={rows}\n  schema={{\n    title: \"Orders\",                     // required for accessibility (or pass aria-label)\n    columns: [\n      { field: \"id\", header: \"Order\", type: \"link\", link: { hrefTemplate: \"/orders/{id}\" } },\n      { field: \"customer.name\", header: \"Customer\", type: \"avatar\", avatar: { subtitleField: \"customer.email\" } },\n      { field: \"status\", header: \"Status\", type: \"badge\", badge: { tones: { paid: \"success\", overdue: \"danger\" } } },\n      { field: \"total\", header: \"Total\", type: \"currency\", format: { currency: \"USD\" } },\n      { field: \"created\", header: \"Created\", type: \"date\", format: { dateStyle: \"relative\" } },\n      { field: \"actions\", header: \"Actions\", type: \"actions\", actions: [{ id: \"refund\", label: \"Refund\", tone: \"danger\" }] },\n    ],\n    features: { selection: \"multi\" },\n    bulkActions: [{ id: \"export\", label: \"Export\" }],\n  }}\n  onAction={(actionId, row) => {}}\n  onBulkAction={(actionId, rows) => {}}\n/>\n```\n\n## Rules\n\n1. **Validate every schema** you write: `parseTableSchema(schema)` from `@/components/tablekit`. The schema is strict, so unknown keys are errors. Fix every error it reports.\n2. Column `type` is one of: `text | number | currency | date | badge | avatar | link | boolean | actions | button`. Type-specific keys (`badge`, `avatar`, `link`, `actions`) are only valid on their own type.\n3. **Colors:** use badge `tones` only: `neutral | info | success | warning | danger | accent`. Never put hex, rgb or Tailwind color classes on the table.\n4. **Styling:** override `--tk-*` CSS variables, or import a preset (`@tablekit/tokens/presets/shadcn.css`, `material3.css`, `carbon.css`, `radix.css`). Don't add utility classes to table internals.\n5. **Font:** the table is designed for Inter (`npm i @fontsource-variable/inter`, then `import \"@fontsource-variable/inter\"`). Numbers use Inter's `\"tnum\"`, `\"zero\"` and `\"ss01\"` via `--tk-font-feature-numeric`. Don't override `font-variant-numeric` in cells.\n6. **Data stays raw:** numbers as numbers, dates as ISO strings or `Date`, enums as strings. `type` and `format` handle display.\n7. **Server-side data:** `manual`, `rowCount`, `state` and `onStateChange`. Don't paginate on the client when the API paginates.\n8. **Mobile:** set `priority` (1 = most important … 5) on secondary columns. `appearance.responsive` is `stack` (cards, the default), `priority` (drop columns) or `scroll`.\n9. **Badge styles:** `badge.fill` (default true) and `badge.stroke` (default false). Outline badges = `fill: false, stroke: true`; tinted + bordered = `stroke: true`. Prefer `indicator: \"icon\"` with `icons` for statuses users scan (payments, deploys).\n10. **Logos** (banks, merchants, payment providers): an avatar column with `avatar: { imageField, logo: \"inline\" }` (16px mark centred on the name's first line, no container) or `logo: \"circle\"` (mark in a neutral/accent circle). Use the brand's compact symbol mark, not its wordmark. Fills follow the theme (dark in dark mode), so give navy/black marks a light variant via `avatar.imageDarkField`.\n11. **Actions:** several row actions → `type: \"actions\"` (⋯ menu, or `actionsDisplay: \"inline\"` icons). Never a row of text buttons. One clear call to action per row → a `type: \"button\"` column.\n12. **Column layout:** the Columns menu lets users show/hide (`features.columnVisibility`), pin (`features.columnPinning`) and reorder (`features.columnReorder`) columns. All three are on by default. `column.pinned: true` sets the initial pin; pin the identifying column (name, ID), not numbers. To remember a user's layout, persist `state.columnOrder`, `state.columnPinning` and `state.columnVisibility` from `onStateChange`.\n13. **Icons:** the table uses Lucide (`lucide-react`). Match it in custom cells rather than mixing icon sets.\n14. Need custom cell markup? Switch to composable mode: `useTable` with `ReactColumnDef.cell` and `<Table.Root>` / `<Table.Toolbar>` / `<Table.Content>` / `<Table.Pagination>`.\n15. Don't wrap `DataTable` in your own `<table>`, and don't re-implement sorting, filtering or pagination around it.\n\n## Props cheat sheet\n\n`schema`, `data`, `loading`, `error`, `onRetry`, `onAction`, `onBulkAction`, `onRowClick`, `onSelectionChange`, `state`, `onStateChange`, `manual`, `rowCount`, `density`, `theme` (`\"light\" | \"dark\"`), `maxHeight`, `toolbarExtra`, `labels` (i18n), `className`, `style`.\n"
    }
  ]
}