{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "type": "registry:ui",
  "title": "Data Table",
  "description": "A sortable, selectable table on real <table> semantics with multi-sort, resizable columns, density and paging.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [],
  "files": [
    {
      "path": "registry/wancraft/data-table.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport type SortDirection = \"asc\" | \"desc\";\nexport interface SortRule {\n  key: string;\n  direction: SortDirection;\n}\n\nexport interface DataTableColumn<T> {\n  /** Unique column key; also the property read from each row when no `cell`/`sortValue` is given. */\n  key: string;\n  /** Header content. Plain strings are used for accessible names of the sort and resize controls. */\n  header: React.ReactNode;\n  /** Custom cell renderer. */\n  cell?: (row: T) => React.ReactNode;\n  /** Allow sorting by this column. */\n  sortable?: boolean;\n  /** Value used for sorting (defaults to `row[key]`). */\n  sortValue?: (row: T) => string | number | null | undefined;\n  align?: \"left\" | \"right\" | \"center\";\n  /** Initial width in px (default 160). */\n  width?: number;\n  /** Minimum width in px when resizing (default 64). */\n  minWidth?: number;\n}\n\nexport type Density = \"comfortable\" | \"compact\";\n\nexport interface DataTableProps<T>\n  extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\" | \"onChange\"> {\n  columns: DataTableColumn<T>[];\n  rows: T[];\n  /** Stable id for each row (used for selection and React keys). */\n  getRowId: (row: T) => string;\n  /** Table caption — names the table for assistive tech. */\n  caption: string;\n  /** Show the caption visually in the toolbar (it is always present for screen readers). */\n  showCaption?: boolean;\n  /** Row selection checkboxes with a tri-state \"select all\" header. */\n  selectable?: boolean;\n  selected?: string[];\n  defaultSelected?: string[];\n  onSelectedChange?: (ids: string[]) => void;\n  /** Controlled sort rules (first rule is the primary sort). */\n  sort?: SortRule[];\n  defaultSort?: SortRule[];\n  onSortChange?: (sort: SortRule[]) => void;\n  /** Shift + click (or Shift + Enter) on a header adds it as a secondary sort. */\n  multiSort?: boolean;\n  /** Drag or keyboard-resize column widths. */\n  resizable?: boolean;\n  /** Paged footer or a \"Load more\" button. */\n  pagination?: \"pages\" | \"load-more\";\n  pageSize?: number;\n  density?: Density;\n  defaultDensity?: Density;\n  onDensityChange?: (density: Density) => void;\n  /** Show the comfortable / compact toggle. */\n  showDensityToggle?: boolean;\n  /** Keep the header row visible while the body scrolls. */\n  stickyHeader?: boolean;\n  /** Scroll area max height in px (0 = no limit). */\n  maxHeight?: number;\n  loading?: boolean;\n  /** Shown when there are no rows. */\n  emptyMessage?: React.ReactNode;\n  /** Selection tint, sort indicator, focus rings and resize handle. */\n  accent?: string;\n}\n\nconst DEFAULT_WIDTH = 160;\nconst SELECT_WIDTH = 44;\nconst collator = new Intl.Collator(\"en\", { numeric: true, sensitivity: \"base\" });\n\nfunction compareValues(a: unknown, b: unknown) {\n  const empty = (v: unknown) => v === null || v === undefined || v === \"\";\n  if (empty(a) && empty(b)) return 0;\n  if (empty(a)) return 1; // empty values sort last in both directions\n  if (empty(b)) return -1;\n  if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n  return collator.compare(String(a), String(b));\n}\n\nconst headerText = (header: React.ReactNode, fallback: string) =>\n  typeof header === \"string\" || typeof header === \"number\" ? String(header) : fallback;\n\n/**\n * A sortable, selectable data table on real <table> semantics: headers sort on click or Enter\n * (Shift adds a secondary sort), rows select with checkboxes and a tri-state header, columns\n * resize by drag or arrow keys, and the body pages or loads more.\n */\nexport function DataTable<T>({\n  columns,\n  rows,\n  getRowId,\n  caption,\n  showCaption = true,\n  selectable = true,\n  selected: selectedProp,\n  defaultSelected,\n  onSelectedChange,\n  sort: sortProp,\n  defaultSort,\n  onSortChange,\n  multiSort = true,\n  resizable = true,\n  pagination = \"pages\",\n  pageSize = 10,\n  density: densityProp,\n  defaultDensity = \"comfortable\",\n  onDensityChange,\n  showDensityToggle = true,\n  stickyHeader = true,\n  maxHeight = 380,\n  loading = false,\n  emptyMessage = \"No rows to show.\",\n  accent = \"#FF7448\",\n  className = \"\",\n  style,\n  ...props\n}: DataTableProps<T>) {\n  const uid = React.useId();\n  const captionId = `${uid}-caption`;\n\n  const [innerSelected, setInnerSelected] = React.useState<string[]>(() => defaultSelected ?? []);\n  const selected = selectedProp ?? innerSelected;\n  const selectedSet = new Set(selected);\n  const [innerSort, setInnerSort] = React.useState<SortRule[]>(() => defaultSort ?? []);\n  const sort = sortProp ?? innerSort;\n  const [innerDensity, setInnerDensity] = React.useState<Density>(defaultDensity);\n  const density = densityProp ?? innerDensity;\n  const [page, setPage] = React.useState(0);\n  const [shown, setShown] = React.useState(pageSize);\n  const [widths, setWidths] = React.useState<Record<string, number>>({});\n  const [announcement, setAnnouncement] = React.useState(\"\");\n\n  const tableRef = React.useRef<HTMLTableElement>(null);\n  const colRefs = React.useRef<Record<string, HTMLTableColElement | null>>({});\n  const lastClicked = React.useRef<number | null>(null);\n  const drag = React.useRef<{ key: string; startX: number; startWidth: number; width: number; pointerId: number } | null>(null);\n\n  const widthOf = (c: DataTableColumn<T>) => widths[c.key] ?? c.width ?? DEFAULT_WIDTH;\n  const totalWidth = columns.reduce((sum, c) => sum + widthOf(c), selectable ? SELECT_WIDTH : 0);\n\n  /* ------------------------------- sorting ------------------------------- */\n\n  const valueOf = (row: T, column: DataTableColumn<T>) =>\n    column.sortValue ? column.sortValue(row) : (row as Record<string, unknown>)[column.key];\n\n  const sortedRows = (() => {\n    const rules = sort\n      .map((r) => ({ rule: r, column: columns.find((c) => c.key === r.key) }))\n      .filter((x): x is { rule: SortRule; column: DataTableColumn<T> } => Boolean(x.column));\n    if (!rules.length) return rows;\n    return rows\n      .map((row, index) => ({ row, index }))\n      .sort((a, b) => {\n        for (const { rule, column } of rules) {\n          const va = valueOf(a.row, column);\n          const vb = valueOf(b.row, column);\n          const empty = va === null || va === undefined || va === \"\" || vb === null || vb === undefined || vb === \"\";\n          const c = compareValues(va, vb);\n          // Empty values stay last regardless of direction.\n          if (c !== 0) return empty || rule.direction === \"asc\" ? c : -c;\n        }\n        return a.index - b.index; // stable\n      })\n      .map((x) => x.row);\n  })();\n\n  const setSort = (next: SortRule[]) => {\n    if (sortProp === undefined) setInnerSort(next);\n    onSortChange?.(next);\n    const primary = next[0];\n    const name = (key: string) => headerText(columns.find((c) => c.key === key)?.header, key);\n    setAnnouncement(\n      primary\n        ? `Sorted by ${next.map((r) => `${name(r.key)}, ${r.direction === \"asc\" ? \"ascending\" : \"descending\"}`).join(\", then \")}.`\n        : \"Sorting removed.\",\n    );\n  };\n\n  const activateHeader = (key: string, additive: boolean) => {\n    const index = sort.findIndex((r) => r.key === key);\n    const current = sort[index];\n    if (additive && multiSort) {\n      if (!current) setSort([...sort, { key, direction: \"asc\" }]);\n      else if (current.direction === \"asc\") setSort(sort.map((r) => (r.key === key ? { key, direction: \"desc\" } : r)));\n      else setSort(sort.filter((r) => r.key !== key));\n      return;\n    }\n    // Plain click: asc → desc → off on the primary column; any other column becomes the only sort.\n    if (!current || sort.length > 1) setSort([{ key, direction: current?.direction === \"asc\" && sort.length === 1 ? \"desc\" : \"asc\" }]);\n    else if (current.direction === \"asc\") setSort([{ key, direction: \"desc\" }]);\n    else setSort([]);\n  };\n\n  /* ------------------------------ pagination ----------------------------- */\n\n  const pageCount = Math.max(1, Math.ceil(sortedRows.length / pageSize));\n  const safePage = Math.min(page, pageCount - 1);\n  const visibleRows =\n    pagination === \"pages\"\n      ? sortedRows.slice(safePage * pageSize, safePage * pageSize + pageSize)\n      : sortedRows.slice(0, Math.max(shown, pageSize));\n  const firstIndex = pagination === \"pages\" ? safePage * pageSize : 0;\n\n  const goToPage = (p: number) => {\n    const next = Math.max(0, Math.min(pageCount - 1, p));\n    setPage(next);\n    setAnnouncement(`Page ${next + 1} of ${pageCount}.`);\n  };\n\n  const loadMore = () => {\n    const next = Math.min(sortedRows.length, Math.max(shown, pageSize) + pageSize);\n    setShown(next);\n    setAnnouncement(`Showing ${next} of ${sortedRows.length} rows.`);\n  };\n\n  /* ------------------------------ selection ------------------------------ */\n\n  const commitSelection = (next: string[]) => {\n    if (selectedProp === undefined) setInnerSelected(next);\n    onSelectedChange?.(next);\n  };\n\n  const allIds = rows.map(getRowId);\n  const selectedCount = allIds.filter((id) => selectedSet.has(id)).length;\n  const allSelected = rows.length > 0 && selectedCount === rows.length;\n  const someSelected = selectedCount > 0 && !allSelected;\n\n  const toggleAll = () => {\n    commitSelection(allSelected ? [] : allIds);\n    setAnnouncement(allSelected ? \"Selection cleared.\" : `All ${rows.length} rows selected.`);\n  };\n\n  const toggleRow = (visibleIndex: number, shiftKey: boolean) => {\n    const row = visibleRows[visibleIndex];\n    if (!row) return;\n    const id = getRowId(row);\n    const turnOn = !selectedSet.has(id);\n    const next = new Set(selectedSet);\n    // Shift + click selects (or clears) the range from the last clicked row.\n    if (shiftKey && lastClicked.current !== null && lastClicked.current !== visibleIndex) {\n      const [a, b] = [lastClicked.current, visibleIndex].sort((x, y) => x - y);\n      for (const r of visibleRows.slice(a, b + 1)) {\n        if (turnOn) next.add(getRowId(r));\n        else next.delete(getRowId(r));\n      }\n    } else if (turnOn) next.add(id);\n    else next.delete(id);\n    lastClicked.current = visibleIndex;\n    commitSelection(allIds.filter((x) => next.has(x)));\n  };\n\n  /* ------------------------------- density ------------------------------- */\n\n  const setDensity = (d: Density) => {\n    if (densityProp === undefined) setInnerDensity(d);\n    onDensityChange?.(d);\n  };\n\n  /* ------------------------------- resizing ------------------------------ */\n\n  const clampWidth = (c: DataTableColumn<T>, w: number) => Math.round(Math.max(c.minWidth ?? 64, Math.min(640, w)));\n\n  const resizeTo = (c: DataTableColumn<T>, w: number) => {\n    setWidths((prev) => ({ ...prev, [c.key]: clampWidth(c, w) }));\n  };\n\n  // Dragging writes widths straight to the <col> and <table> (no re-render per move); state is committed on release.\n  const onResizePointerDown = (c: DataTableColumn<T>, event: React.PointerEvent<HTMLDivElement>) => {\n    if (event.button !== 0) return;\n    event.preventDefault();\n    event.currentTarget.setPointerCapture(event.pointerId);\n    drag.current = { key: c.key, startX: event.clientX, startWidth: widthOf(c), width: widthOf(c), pointerId: event.pointerId };\n  };\n\n  const onResizePointerMove = (c: DataTableColumn<T>, event: React.PointerEvent<HTMLDivElement>) => {\n    const d = drag.current;\n    if (!d || d.key !== c.key || d.pointerId !== event.pointerId) return;\n    const w = clampWidth(c, d.startWidth + event.clientX - d.startX);\n    if (w === d.width) return;\n    const col = colRefs.current[c.key];\n    if (col) col.style.width = `${w}px`;\n    if (tableRef.current) tableRef.current.style.width = `${totalWidth - d.startWidth + w}px`;\n    event.currentTarget.setAttribute(\"aria-valuenow\", String(w));\n    d.width = w;\n  };\n\n  const onResizePointerUp = (c: DataTableColumn<T>, event: React.PointerEvent<HTMLDivElement>) => {\n    const d = drag.current;\n    if (!d || d.key !== c.key) return;\n    drag.current = null;\n    if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);\n    resizeTo(c, d.width);\n  };\n\n  const onResizeKeyDown = (c: DataTableColumn<T>, event: React.KeyboardEvent<HTMLDivElement>) => {\n    const step = event.shiftKey ? 40 : 10;\n    const w = widthOf(c);\n    let next: number | null = null;\n    if (event.key === \"ArrowRight\" || event.key === \"ArrowUp\") next = w + step;\n    else if (event.key === \"ArrowLeft\" || event.key === \"ArrowDown\") next = w - step;\n    else if (event.key === \"Home\") next = c.minWidth ?? 64;\n    else if (event.key === \"End\") next = 640;\n    else if (event.key === \"Enter\" && widths[c.key] !== undefined) {\n      // Enter restores the column's default width.\n      setWidths((prev) => {\n        const copy = { ...prev };\n        delete copy[c.key];\n        return copy;\n      });\n      event.preventDefault();\n      return;\n    }\n    if (next === null) return;\n    event.preventDefault();\n    resizeTo(c, next);\n  };\n\n  /* -------------------------------- render ------------------------------- */\n\n  const cellPad = density === \"compact\" ? \"px-3 py-1.5\" : \"px-4 py-3\";\n  const alignClass = (a: DataTableColumn<T>[\"align\"]) => (a === \"right\" ? \"text-right\" : a === \"center\" ? \"text-center\" : \"text-left\");\n  const colSpan = columns.length + (selectable ? 1 : 0);\n  const focusRing =\n    \"outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--dt-accent)]\";\n  const checkboxClass = `size-4 cursor-pointer rounded accent-[var(--dt-accent)] ${focusRing}`;\n\n  const rangeLabel =\n    rows.length === 0\n      ? \"0 rows\"\n      : pagination === \"pages\"\n        ? `${firstIndex + 1}–${firstIndex + visibleRows.length} of ${sortedRows.length}`\n        : `${visibleRows.length} of ${sortedRows.length}`;\n\n  return (\n    <div\n      className={`flex w-full min-w-0 flex-col overflow-hidden rounded-xl border border-zinc-900/10 bg-white dark:border-white/10 dark:bg-zinc-950 ${className}`}\n      style={{ [\"--dt-accent\" as string]: accent, ...style }}\n      {...props}\n    >\n      {/* Toolbar */}\n      <div className=\"flex min-h-12 flex-wrap items-center justify-between gap-x-4 gap-y-2 border-b border-zinc-900/10 px-4 py-2 dark:border-white/10\">\n        <div className=\"flex min-w-0 items-baseline gap-3\">\n          {showCaption && <span className=\"truncate text-[14px] font-semibold tracking-tight text-zinc-950 dark:text-white\">{caption}</span>}\n          <span className=\"font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 dark:text-zinc-400\">\n            {rows.length} rows\n            {selectable && selectedCount > 0 && <span className=\"text-zinc-900 dark:text-zinc-100\"> · {selectedCount} selected</span>}\n          </span>\n          {selectable && selectedCount > 0 && (\n            <button\n              type=\"button\"\n              onClick={() => {\n                commitSelection([]);\n                setAnnouncement(\"Selection cleared.\");\n              }}\n              className={`rounded font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 underline decoration-zinc-900/20 underline-offset-[3px] hover:text-zinc-950 dark:text-zinc-400 dark:decoration-white/20 dark:hover:text-white ${focusRing}`}\n            >\n              Clear\n            </button>\n          )}\n        </div>\n        {showDensityToggle && (\n          <div role=\"group\" aria-label=\"Row density\" className=\"flex rounded-lg border border-zinc-900/10 p-0.5 dark:border-white/10\">\n            {([\"comfortable\", \"compact\"] as const).map((d) => (\n              <button\n                key={d}\n                type=\"button\"\n                aria-pressed={density === d}\n                onClick={() => setDensity(d)}\n                className={`rounded-md px-2 py-1 font-mono text-[10px] uppercase tracking-[0.12em] transition-colors ${focusRing} ${\n                  density === d\n                    ? \"bg-zinc-900 text-white dark:bg-white dark:text-zinc-950\"\n                    : \"text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white\"\n                }`}\n              >\n                {d}\n              </button>\n            ))}\n          </div>\n        )}\n      </div>\n\n      {/* Scroll area: focusable so keyboard users can scroll wide or tall tables. */}\n      <div\n        role=\"region\"\n        aria-labelledby={captionId}\n        tabIndex={0}\n        className={`relative min-w-0 overflow-auto overscroll-contain ${focusRing} focus-visible:-outline-offset-2`}\n        style={{ maxHeight: maxHeight > 0 ? maxHeight : undefined }}\n      >\n        <table\n          ref={tableRef}\n          aria-busy={loading || undefined}\n          aria-describedby={`${uid}-help`}\n          className=\"table-fixed border-separate border-spacing-0 text-[13px] text-zinc-700 dark:text-zinc-300\"\n          style={{ width: totalWidth, minWidth: \"100%\" }}\n        >\n          <caption id={captionId} className=\"sr-only\">\n            {caption}\n          </caption>\n          <colgroup>\n            {selectable && <col style={{ width: SELECT_WIDTH }} />}\n            {columns.map((c) => (\n              <col\n                key={c.key}\n                ref={(el) => {\n                  colRefs.current[c.key] = el;\n                }}\n                style={{ width: widthOf(c) }}\n              />\n            ))}\n          </colgroup>\n          <thead>\n            <tr>\n              {selectable && (\n                <th\n                  scope=\"col\"\n                  className={`${stickyHeader ? \"sticky top-0 z-10\" : \"\"} border-b border-zinc-900/10 bg-zinc-50 px-0 text-center dark:border-white/10 dark:bg-zinc-900`}\n                >\n                  <input\n                    type=\"checkbox\"\n                    aria-label={allSelected ? \"Deselect all rows\" : `Select all ${rows.length} rows`}\n                    checked={allSelected}\n                    disabled={!rows.length || loading}\n                    ref={(el) => {\n                      if (el) el.indeterminate = someSelected;\n                    }}\n                    onChange={toggleAll}\n                    className={`${checkboxClass} align-middle`}\n                  />\n                </th>\n              )}\n              {columns.map((c) => {\n                const ruleIndex = sort.findIndex((r) => r.key === c.key);\n                const rule = sort[ruleIndex];\n                const name = headerText(c.header, c.key);\n                const ariaSort = ruleIndex === 0 ? (rule.direction === \"asc\" ? \"ascending\" : \"descending\") : undefined;\n                return (\n                  <th\n                    key={c.key}\n                    scope=\"col\"\n                    aria-sort={c.sortable ? (ariaSort ?? \"none\") : undefined}\n                    className={`group/th relative border-b border-zinc-900/10 bg-zinc-50 font-mono text-[10px] font-normal uppercase tracking-[0.14em] text-zinc-500 dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-400 ${\n                      stickyHeader ? \"sticky top-0 z-10\" : \"\"\n                    } ${alignClass(c.align)}`}\n                  >\n                    {c.sortable ? (\n                      <button\n                        type=\"button\"\n                        onClick={(e) => activateHeader(c.key, e.shiftKey)}\n                        onKeyDown={(e) => {\n                          // Native buttons fire click on Enter without shiftKey in some browsers; handle Shift+Enter here.\n                          if (e.key === \"Enter\" && e.shiftKey) {\n                            e.preventDefault();\n                            activateHeader(c.key, true);\n                          }\n                        }}\n                        className={`flex w-full items-center gap-1.5 ${density === \"compact\" ? \"px-3 py-2\" : \"px-4 py-2.5\"} uppercase tracking-[0.14em] transition-colors hover:text-zinc-950 dark:hover:text-white ${\n                          c.align === \"right\" ? \"flex-row-reverse text-right\" : c.align === \"center\" ? \"justify-center\" : \"\"\n                        } ${rule ? \"text-zinc-950 dark:text-white\" : \"\"} outline-hidden focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--dt-accent)]`}\n                      >\n                        <span className=\"truncate\">{c.header}</span>\n                        <SortGlyph direction={rule?.direction} />\n                        {rule && sort.length > 1 && (\n                          <span aria-hidden=\"true\" className=\"font-mono text-[9px] tabular-nums text-[var(--dt-accent)]\">\n                            {ruleIndex + 1}\n                          </span>\n                        )}\n                        <span className=\"sr-only\">\n                          {rule\n                            ? `, sorted ${rule.direction === \"asc\" ? \"ascending\" : \"descending\"}${sort.length > 1 ? `, priority ${ruleIndex + 1}` : \"\"}`\n                            : \", sortable\"}\n                        </span>\n                      </button>\n                    ) : (\n                      <span className={`block truncate ${density === \"compact\" ? \"px-3 py-2\" : \"px-4 py-2.5\"}`}>{c.header}</span>\n                    )}\n                    {resizable && (\n                      <div\n                        role=\"separator\"\n                        aria-orientation=\"vertical\"\n                        aria-label={`Resize ${name} column`}\n                        aria-valuenow={widthOf(c)}\n                        aria-valuemin={c.minWidth ?? 64}\n                        aria-valuemax={640}\n                        tabIndex={0}\n                        onPointerDown={(e) => onResizePointerDown(c, e)}\n                        onPointerMove={(e) => onResizePointerMove(c, e)}\n                        onPointerUp={(e) => onResizePointerUp(c, e)}\n                        onPointerCancel={(e) => onResizePointerUp(c, e)}\n                        onKeyDown={(e) => onResizeKeyDown(c, e)}\n                        onDoubleClick={() =>\n                          setWidths((prev) => {\n                            const copy = { ...prev };\n                            delete copy[c.key];\n                            return copy;\n                          })\n                        }\n                        className=\"group/rs absolute inset-y-0 right-0 z-20 flex w-2.5 cursor-col-resize touch-none justify-end outline-hidden\"\n                      >\n                        <span\n                          aria-hidden=\"true\"\n                          className=\"my-2 w-px bg-zinc-900/10 transition-[background-color,width] group-hover/rs:w-[2px] group-hover/rs:bg-[var(--dt-accent)] group-focus-visible/rs:w-[2px] group-focus-visible/rs:bg-[var(--dt-accent)] group-active/rs:bg-[var(--dt-accent)] dark:bg-white/10\"\n                        />\n                      </div>\n                    )}\n                  </th>\n                );\n              })}\n            </tr>\n          </thead>\n          <tbody>\n            {loading ? (\n              Array.from({ length: Math.min(pageSize, 8) }, (_, r) => (\n                <tr key={`sk-${r}`} aria-hidden=\"true\">\n                  {selectable && <td className=\"border-b border-zinc-900/[0.06] dark:border-white/[0.06]\" />}\n                  {columns.map((c, ci) => (\n                    <td key={c.key} className={`border-b border-zinc-900/[0.06] dark:border-white/[0.06] ${cellPad}`}>\n                      <span\n                        className={`block h-2.5 animate-pulse rounded-full bg-zinc-900/[0.07] motion-reduce:animate-none dark:bg-white/[0.08] ${c.align === \"right\" ? \"ml-auto\" : \"\"}`}\n                        style={{ width: `${40 + ((r * 7 + ci * 13) % 45)}%` }}\n                      />\n                    </td>\n                  ))}\n                </tr>\n              ))\n            ) : visibleRows.length === 0 ? (\n              <tr>\n                <td colSpan={colSpan} className=\"px-4 py-14 text-center\">\n                  <span className=\"mx-auto mb-3 grid size-9 place-items-center rounded-full border border-dashed border-zinc-900/20 text-zinc-400 dark:border-white/20\">\n                    <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className=\"size-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.4\">\n                      <rect x=\"2.5\" y=\"3\" width=\"11\" height=\"10\" rx=\"1.5\" />\n                      <path d=\"M2.5 6.5h11M6.5 6.5V13\" />\n                    </svg>\n                  </span>\n                  <span className=\"text-[13px] text-zinc-500 dark:text-zinc-400\">{emptyMessage}</span>\n                </td>\n              </tr>\n            ) : (\n              visibleRows.map((row, ri) => {\n                const id = getRowId(row);\n                const isSelected = selectedSet.has(id);\n                return (\n                  <tr\n                    key={id}\n                    data-selected={isSelected || undefined}\n                    className={`transition-colors duration-100 ${\n                      isSelected\n                        ? \"bg-[color-mix(in_srgb,var(--dt-accent)_9%,transparent)]\"\n                        : \"hover:bg-zinc-900/[0.03] dark:hover:bg-white/[0.04]\"\n                    }`}\n                  >\n                    {selectable && (\n                      <td className=\"border-b border-zinc-900/[0.06] text-center dark:border-white/[0.06]\">\n                        <input\n                          type=\"checkbox\"\n                          aria-label={`Select row ${firstIndex + ri + 1}`}\n                          aria-describedby={`${uid}-row-${id}`}\n                          checked={isSelected}\n                          onChange={(e) => toggleRow(ri, (e.nativeEvent as MouseEvent).shiftKey === true)}\n                          className={`${checkboxClass} align-middle`}\n                        />\n                      </td>\n                    )}\n                    {columns.map((c, ci) => (\n                      <td\n                        key={c.key}\n                        id={ci === 0 ? `${uid}-row-${id}` : undefined}\n                        className={`truncate border-b border-zinc-900/[0.06] dark:border-white/[0.06] ${cellPad} ${alignClass(c.align)} ${\n                          c.align === \"right\" ? \"tabular-nums\" : \"\"\n                        }`}\n                      >\n                        {c.cell ? c.cell(row) : String((row as Record<string, unknown>)[c.key] ?? \"\")}\n                      </td>\n                    ))}\n                  </tr>\n                );\n              })\n            )}\n          </tbody>\n        </table>\n      </div>\n\n      {/* Footer */}\n      <div className=\"flex min-h-11 flex-wrap items-center justify-between gap-3 border-t border-zinc-900/10 px-4 py-2 dark:border-white/10\">\n        <span className=\"font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 tabular-nums dark:text-zinc-400\">{loading ? \"Loading…\" : rangeLabel}</span>\n        {!loading && pagination === \"pages\" && pageCount > 1 && (\n          <nav aria-label=\"Pagination\" className=\"flex items-center gap-1\">\n            <PagerButton label=\"Previous page\" disabled={safePage === 0} onClick={() => goToPage(safePage - 1)} focusRing={focusRing}>\n              <path d=\"M10 3.5 5.5 8l4.5 4.5\" />\n            </PagerButton>\n            <span className=\"min-w-16 text-center font-mono text-[10px] uppercase tracking-[0.14em] tabular-nums text-zinc-600 dark:text-zinc-300\">\n              {safePage + 1} / {pageCount}\n            </span>\n            <PagerButton label=\"Next page\" disabled={safePage >= pageCount - 1} onClick={() => goToPage(safePage + 1)} focusRing={focusRing}>\n              <path d=\"M6 3.5 10.5 8 6 12.5\" />\n            </PagerButton>\n          </nav>\n        )}\n        {!loading && pagination === \"load-more\" && visibleRows.length < sortedRows.length && (\n          <button\n            type=\"button\"\n            onClick={loadMore}\n            className={`h-8 rounded-lg border border-zinc-900/12 px-3 text-[12px] font-medium text-zinc-700 transition-colors hover:border-zinc-900/30 hover:text-zinc-950 dark:border-white/12 dark:text-zinc-300 dark:hover:border-white/30 dark:hover:text-white ${focusRing}`}\n          >\n            Load {Math.min(pageSize, sortedRows.length - visibleRows.length)} more\n          </button>\n        )}\n      </div>\n\n      <p id={`${uid}-help`} hidden>\n        {[\n          columns.some((c) => c.sortable) ? `Sortable column headers are buttons${multiSort ? \"; hold Shift to add a secondary sort\" : \"\"}.` : \"\",\n          resizable ? \"Column resize handles take Left and Right arrows.\" : \"\",\n        ].join(\" \")}\n      </p>\n      <span className=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\">\n        {announcement}\n      </span>\n    </div>\n  );\n}\n\nfunction SortGlyph({ direction }: { direction?: SortDirection }) {\n  return (\n    <svg aria-hidden=\"true\" viewBox=\"0 0 10 12\" className=\"size-2.5 shrink-0\" fill=\"currentColor\">\n      <path d=\"M5 1 8.5 5h-7z\" className={direction === \"asc\" ? \"text-[var(--dt-accent)]\" : direction ? \"opacity-20\" : \"opacity-30\"} />\n      <path d=\"M5 11 1.5 7h7z\" className={direction === \"desc\" ? \"text-[var(--dt-accent)]\" : direction ? \"opacity-20\" : \"opacity-30\"} />\n    </svg>\n  );\n}\n\nfunction PagerButton({\n  label,\n  disabled,\n  onClick,\n  focusRing,\n  children,\n}: {\n  label: string;\n  disabled: boolean;\n  onClick: () => void;\n  focusRing: string;\n  children: React.ReactNode;\n}) {\n  return (\n    <button\n      type=\"button\"\n      aria-label={label}\n      disabled={disabled}\n      onClick={onClick}\n      className={`grid size-8 place-items-center rounded-lg border border-zinc-900/12 text-zinc-600 transition-colors hover:border-zinc-900/30 hover:text-zinc-950 disabled:pointer-events-none disabled:opacity-35 dark:border-white/12 dark:text-zinc-300 dark:hover:border-white/30 dark:hover:text-white ${focusRing}`}\n    >\n      <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className=\"size-3.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n        {children}\n      </svg>\n    </button>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "data"
  ],
  "docs": "Documentation: https://wancraft.my/components/data-table"
}
