{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "title": "Data Table",
  "description": "Typed rows with custom cells, column widths you set or the reader drags, sorting that is one property to switch on, and keyed selection.",
  "dependencies": [
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/data-table/data-table.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowDown, ArrowUp, ChevronsUpDown } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\nexport type SortDirection = \"asc\" | \"desc\"\n\nexport type DataTableSort = { column: string; direction: SortDirection }\n\nexport type SortValue = string | number | boolean | Date | null | undefined\n\nexport type Column<TRow> = {\n  /** Identifies the column, and names the field read when there is no value. */\n  key: string\n  header: React.ReactNode\n  /** What the cell shows. Defaults to the field named by key. */\n  cell?: (row: TRow, index: number) => React.ReactNode\n  /** What the column is worth when sorted. Defaults to the field named by key. */\n  value?: (row: TRow) => SortValue\n  /** Any CSS width. Columns without one share what is left over. */\n  width?: string\n  /** Narrowest this column may be dragged, in pixels. Defaults to 64. */\n  minWidth?: number\n  /** Widest it may be dragged. Unbounded by default. */\n  maxWidth?: number\n  align?: \"start\" | \"center\" | \"end\"\n  /** Holds the column against the left edge while the rest scrolls past. */\n  pinned?: \"start\"\n  /** Lets the cell run onto a second line instead of being cut short. */\n  wrap?: boolean\n  /** A summary under the column. Given the rows, in the order shown. */\n  footer?: React.ReactNode | ((rows: readonly TRow[]) => React.ReactNode)\n  /** true for the built-in comparator, or your own. Absent means not sortable. */\n  sort?: boolean | ((a: TRow, b: TRow) => number)\n  /** Which way the first press sorts. Numbers usually want the largest first. */\n  sortFirst?: SortDirection\n  /** Excludes one column while the rest stay resizable. */\n  resizable?: boolean\n  headerClassName?: string\n  cellClassName?: string\n}\n\nexport type DataTableProps<TRow> = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"children\" | \"onSelect\"\n> & {\n  rows: readonly TRow[]\n  columns: readonly Column<TRow>[]\n  /** Identity that survives sorting. Selection is kept in these. */\n  getKey: (row: TRow) => string\n  /** Names a row, for the checkbox that selects it. */\n  getLabel?: (row: TRow) => string\n  /** Names the table itself. Becomes its caption. */\n  label: string\n\n  sort?: DataTableSort | null\n  defaultSort?: DataTableSort\n  onSortChange?: (sort: DataTableSort | null) => void\n\n  selected?: readonly string[]\n  defaultSelected?: readonly string[]\n  onSelectionChange?: (keys: string[]) => void\n\n  /** Lets the reader drag the boundary between two columns. */\n  resizable?: boolean\n  onColumnResize?: (key: string, width: number) => void\n\n  density?: \"comfortable\" | \"compact\"\n  striped?: boolean\n  stickyHeader?: boolean\n  /** Shows placeholder rows shaped like the real ones. */\n  loading?: boolean\n  /** How many placeholder rows to show. Defaults to 5. */\n  loadingRows?: number\n  rowClassName?: (row: TRow, index: number) => string | undefined\n  /**\n   * A convenience for the pointer. It is never the only way to reach whatever\n   * it does: put a link or a button in a cell for that.\n   */\n  onRowClick?: (row: TRow, index: number) => void\n  empty?: React.ReactNode\n}\n\nconst MIN_WIDTH = 64\nconst KEY_STEP = 16\n\n/** Held back so a fast answer never flashes a skeleton on the way past. */\nconst LOADING_DELAY_MS = 120\n\n/**\n * The same shades as muted/60 and the rest, mixed against the surface rather\n * than laid over it. A held column is painted over whatever is sliding beneath\n * it, and a background that is only mostly opaque shows both at once.\n */\nconst SURFACE = {\n  head: \"bg-[color-mix(in_oklch,var(--muted)_60%,var(--card))]\",\n  stripe: \"even:bg-[color-mix(in_oklch,var(--muted)_40%,var(--card))]\",\n  hover: \"hover:bg-[color-mix(in_oklch,var(--muted)_40%,var(--card))]\",\n  picked: \"bg-[color-mix(in_oklch,var(--primary)_7%,var(--card))]\",\n  pickedHover: \"hover:bg-[color-mix(in_oklch,var(--primary)_12%,var(--card))]\",\n  foot: \"bg-[color-mix(in_oklch,var(--muted)_40%,var(--card))]\",\n} as const\n\nconst ALIGN = {\n  start: \"text-left\",\n  center: \"text-center\",\n  end: \"text-right tabular-nums\",\n} as const\n\nfunction readValue<TRow>(column: Column<TRow>, row: TRow): SortValue {\n  if (column.value) return column.value(row)\n  return (row as Record<string, unknown>)[column.key] as SortValue\n}\n\nfunction isBlank(value: SortValue) {\n  return value === null || value === undefined || value === \"\"\n}\n\n/** Numbers as numbers, dates as dates, and everything else by the locale. */\nfunction compare(a: SortValue, b: SortValue): number {\n  if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime()\n  if (typeof a === \"number\" && typeof b === \"number\") return a - b\n  if (typeof a === \"boolean\" && typeof b === \"boolean\") {\n    return Number(a) - Number(b)\n  }\n\n  const left = String(a)\n  const right = String(b)\n  const leftNumber = Number(left)\n  const rightNumber = Number(right)\n\n  if (!Number.isNaN(leftNumber) && !Number.isNaN(rightNumber)) {\n    return leftNumber - rightNumber\n  }\n\n  return left.localeCompare(right)\n}\n\nexport function DataTable<TRow>({\n  rows,\n  columns,\n  getKey,\n  getLabel,\n  label,\n  sort,\n  defaultSort,\n  onSortChange,\n  selected,\n  defaultSelected,\n  onSelectionChange,\n  resizable = false,\n  onColumnResize,\n  density = \"comfortable\",\n  striped = false,\n  stickyHeader = false,\n  loading = false,\n  loadingRows = 5,\n  rowClassName,\n  onRowClick,\n  empty = \"Nothing to show.\",\n  className,\n  ...rootProps\n}: DataTableProps<TRow>) {\n  const reactId = React.useId()\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const tableRef = React.useRef<HTMLTableElement>(null)\n  const cols = React.useRef<(HTMLTableColElement | null)[]>([])\n  const heads = React.useRef<(HTMLTableCellElement | null)[]>([])\n  const frozen = React.useRef(false)\n  const lastPicked = React.useRef<number | null>(null)\n  const [delayPassed, setDelayPassed] = React.useState(false)\n\n  const [uncontrolledSort, setUncontrolledSort] =\n    React.useState<DataTableSort | null>(defaultSort ?? null)\n  const activeSort = sort !== undefined ? sort : uncontrolledSort\n\n  const [uncontrolledSelection, setUncontrolledSelection] = React.useState<\n    string[]\n  >([...(defaultSelected ?? [])])\n  const selection = selected ? [...selected] : uncontrolledSelection\n  const selectable =\n    selected !== undefined ||\n    defaultSelected !== undefined ||\n    onSelectionChange !== undefined\n\n  React.useEffect(() => {\n    // Cleared a tick after the data lands rather than during it, so the next\n    // load waits its turn again. Nothing is on screen either way, because the\n    // placeholders are only shown while loading is still true.\n    const timer = window.setTimeout(\n      () => setDelayPassed(loading),\n      loading ? LOADING_DELAY_MS : 0\n    )\n\n    return () => window.clearTimeout(timer)\n  }, [loading])\n\n  const skeleton = loading && delayPassed\n\n  const setSort = (next: DataTableSort | null) => {\n    if (sort === undefined) setUncontrolledSort(next)\n    onSortChange?.(next)\n  }\n\n  const setSelection = (next: string[]) => {\n    if (selected === undefined) setUncontrolledSelection(next)\n    onSelectionChange?.(next)\n  }\n\n  const ordered = React.useMemo(() => {\n    if (!activeSort) return [...rows]\n\n    const column = columns.find((entry) => entry.key === activeSort.column)\n    if (!column?.sort) return [...rows]\n\n    const reverse = activeSort.direction === \"desc\" ? -1 : 1\n    const custom = typeof column.sort === \"function\" ? column.sort : undefined\n\n    return [...rows].sort((a, b) => {\n      if (custom) return custom(a, b) * reverse\n\n      const left = readValue(column, a)\n      const right = readValue(column, b)\n\n      // Blanks sit at the bottom whichever way the column is pointing, because\n      // a column of empty cells at the top is never what was being asked for.\n      const leftBlank = isBlank(left)\n      const rightBlank = isBlank(right)\n      if (leftBlank && rightBlank) return 0\n      if (leftBlank) return 1\n      if (rightBlank) return -1\n\n      return compare(left, right) * reverse\n    })\n  }, [activeSort, columns, rows])\n\n  const keys = ordered.map(getKey)\n  const chosen = new Set(selection)\n  const picked = keys.filter((key) => chosen.has(key))\n  const allPicked = keys.length > 0 && picked.length === keys.length\n  const somePicked = picked.length > 0 && !allPicked\n\n  const headerCheckbox = React.useRef<HTMLInputElement>(null)\n  React.useEffect(() => {\n    if (headerCheckbox.current)\n      headerCheckbox.current.indeterminate = somePicked\n  })\n\n  const cycle = (column: Column<TRow>) => {\n    if (!column.sort) return\n\n    const first = column.sortFirst ?? \"asc\"\n\n    if (activeSort?.column !== column.key) {\n      setSort({ column: column.key, direction: first })\n      return\n    }\n\n    // Third press returns the rows to the order they arrived in.\n    setSort(\n      activeSort.direction === first\n        ? { column: column.key, direction: first === \"asc\" ? \"desc\" : \"asc\" }\n        : null\n    )\n  }\n\n  const toggleRow = (index: number, key: string, range: boolean) => {\n    const next = new Set(selection)\n\n    if (range && lastPicked.current !== null) {\n      const [from, to] = [lastPicked.current, index].sort((a, b) => a - b)\n      const shouldSelect = !chosen.has(key)\n\n      for (let at = from!; at <= to!; at += 1) {\n        const rowKey = keys[at]\n        if (rowKey === undefined) continue\n        if (shouldSelect) next.add(rowKey)\n        else next.delete(rowKey)\n      }\n    } else if (chosen.has(key)) {\n      next.delete(key)\n    } else {\n      next.add(key)\n    }\n\n    lastPicked.current = index\n    setSelection([...next])\n  }\n\n  /**\n   * Every column is pinned to the width it currently has before the first drag,\n   * so pulling one boundary does not make every other column jump.\n   */\n  const freezeWidths = () => {\n    if (frozen.current) return\n    for (const col of cols.current) {\n      if (col) col.style.width = `${col.getBoundingClientRect().width}px`\n    }\n    frozen.current = true\n  }\n\n  const resizeTo = (index: number, width: number, column: Column<TRow>) => {\n    const col = cols.current[index]\n    if (!col) return\n\n    let next = Math.max(column.minWidth ?? MIN_WIDTH, Math.round(width))\n    if (column.maxWidth !== undefined) next = Math.min(column.maxWidth, next)\n\n    col.style.width = `${next}px`\n    syncPins()\n    return next\n  }\n\n  const cellPadding = density === \"compact\" ? \"px-3 py-1.5\" : \"px-3 py-2.5\"\n  const offset = selectable ? 1 : 0\n\n  // Visual positions of the held columns. The checkbox is held too whenever\n  // anything else is, since a column of checkboxes that scrolls away from its\n  // rows is worse than none.\n  const pinned = React.useMemo(() => {\n    const held = columns\n      .map((column, index) => (column.pinned === \"start\" ? index + offset : -1))\n      .filter((index) => index >= 0)\n\n    if (held.length === 0) return []\n    return selectable ? [0, ...held] : held\n  }, [columns, offset, selectable])\n\n  const lastPinned = pinned[pinned.length - 1]\n  const pinKey = pinned.join()\n\n  /**\n   * Writes each held column's distance from the left edge as a custom\n   * property, so the offsets follow a drag without anything re-rendering.\n   */\n  const syncPins = React.useCallback(() => {\n    const root = rootRef.current\n    if (!root || pinKey === \"\") return\n\n    let left = 0\n    for (const index of pinKey.split(\",\").map(Number)) {\n      root.style.setProperty(`--pin-${index}`, `${left}px`)\n      left += heads.current[index]?.getBoundingClientRect().width ?? 0\n    }\n  }, [pinKey])\n\n  React.useLayoutEffect(() => {\n    syncPins()\n\n    const table = tableRef.current\n    if (!table || pinKey === \"\") return\n\n    const observer = new ResizeObserver(syncPins)\n    observer.observe(table)\n    return () => observer.disconnect()\n  }, [pinKey, syncPins])\n\n  const pinStyle = (index: number): React.CSSProperties | undefined =>\n    pinned.includes(index) ? { left: `var(--pin-${index})` } : undefined\n\n  const pinClass = (index: number, layer: string) =>\n    pinned.includes(index) &&\n    cn(\"sticky\", layer, index === lastPinned && \"border-border border-r\")\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"data-table\"\n      className={cn(\n        \"border-border bg-card text-card-foreground relative overflow-auto rounded-[var(--radius)] border\",\n        className\n      )}\n      {...rootProps}\n    >\n      <table\n        ref={tableRef}\n        aria-busy={loading || undefined}\n        className=\"w-full table-fixed border-collapse text-sm\"\n      >\n        <caption className=\"sr-only\">{label}</caption>\n\n        <colgroup>\n          {selectable && (\n            <col\n              ref={(node) => {\n                cols.current[0] = node\n              }}\n              style={{ width: \"2.75rem\" }}\n            />\n          )}\n          {columns.map((column, index) => (\n            <col\n              key={column.key}\n              ref={(node) => {\n                cols.current[index + offset] = node\n              }}\n              style={column.width ? { width: column.width } : undefined}\n            />\n          ))}\n        </colgroup>\n\n        <thead\n          className={cn(SURFACE.head, stickyHeader && \"sticky top-0 z-20\")}\n        >\n          <tr>\n            {selectable && (\n              <th\n                scope=\"col\"\n                ref={(node) => {\n                  heads.current[0] = node\n                }}\n                style={pinStyle(0)}\n                className={cn(\n                  \"border-border border-b px-3\",\n                  SURFACE.head,\n                  pinClass(0, \"z-30\")\n                )}\n              >\n                <input\n                  ref={headerCheckbox}\n                  type=\"checkbox\"\n                  className=\"accent-primary focus-visible:ring-ring size-4 align-middle focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none\"\n                  checked={allPicked}\n                  aria-label={\n                    allPicked\n                      ? `Clear all ${keys.length} rows`\n                      : `Select all ${keys.length} rows`\n                  }\n                  onChange={() => {\n                    lastPicked.current = null\n                    setSelection(allPicked ? [] : keys)\n                  }}\n                />\n              </th>\n            )}\n\n            {columns.map((column, index) => {\n              const active = activeSort?.column === column.key\n              const direction = active ? activeSort.direction : undefined\n              const Icon =\n                direction === \"asc\"\n                  ? ArrowUp\n                  : direction === \"desc\"\n                    ? ArrowDown\n                    : ChevronsUpDown\n\n              // Boundaries between columns, so no handle on the last one:\n              // there is nothing to its right to trade width with, and it\n              // would sit on the container's own edge.\n              const canResize =\n                resizable &&\n                column.resizable !== false &&\n                index < columns.length - 1\n              const at = index + offset\n\n              return (\n                <th\n                  key={column.key}\n                  scope=\"col\"\n                  ref={(node) => {\n                    heads.current[at] = node\n                  }}\n                  style={pinStyle(at)}\n                  aria-sort={\n                    !column.sort\n                      ? undefined\n                      : active\n                        ? direction === \"asc\"\n                          ? \"ascending\"\n                          : \"descending\"\n                        : \"none\"\n                  }\n                  className={cn(\n                    \"border-border text-muted-foreground relative border-b p-0 text-xs font-semibold\",\n                    SURFACE.head,\n                    pinClass(at, \"z-30\"),\n                    column.headerClassName\n                  )}\n                >\n                  {column.sort ? (\n                    <button\n                      type=\"button\"\n                      onClick={() => cycle(column)}\n                      className={cn(\n                        \"group focus-visible:ring-ring hover:text-foreground flex min-h-11 w-full items-center gap-1.5 px-3 transition-colors duration-150 focus-visible:ring-2 focus-visible:-outline-offset-2 focus-visible:outline-none motion-reduce:transition-none\",\n                        column.align === \"end\" && \"justify-end\",\n                        column.align === \"center\" && \"justify-center\"\n                      )}\n                    >\n                      <span className=\"truncate\">{column.header}</span>\n                      <Icon\n                        aria-hidden=\"true\"\n                        size={13}\n                        className={cn(\n                          \"shrink-0 transition-opacity duration-150 motion-reduce:transition-none\",\n                          active\n                            ? \"text-foreground opacity-100\"\n                            : \"opacity-0 group-hover:opacity-40 group-focus-visible:opacity-40\"\n                        )}\n                      />\n                    </button>\n                  ) : (\n                    <div\n                      className={cn(\n                        \"flex min-h-11 items-center px-3\",\n                        ALIGN[column.align ?? \"start\"],\n                        column.align === \"end\" && \"justify-end\",\n                        column.align === \"center\" && \"justify-center\"\n                      )}\n                    >\n                      <span className=\"truncate\">{column.header}</span>\n                    </div>\n                  )}\n\n                  {canResize && (\n                    <span\n                      role=\"separator\"\n                      tabIndex={0}\n                      aria-orientation=\"vertical\"\n                      aria-label={`Resize column ${column.key}`}\n                      data-slot=\"data-table-resizer\"\n                      className=\"group/resize absolute inset-y-0 right-0 z-10 flex w-px cursor-col-resize touch-none justify-center focus-visible:outline-none\"\n                      onDoubleClick={() => {\n                        const col = cols.current[at]\n                        if (!col) return\n                        col.style.width = column.width ?? \"\"\n                        frozen.current = false\n                      }}\n                      onPointerDown={(event) => {\n                        if (event.button !== 0) return\n                        event.preventDefault()\n                        event.currentTarget.setPointerCapture(event.pointerId)\n                        freezeWidths()\n\n                        const col = cols.current[at]\n                        const startX = event.clientX\n                        const startWidth =\n                          col?.getBoundingClientRect().width ?? 0\n\n                        const move = (moveEvent: PointerEvent) => {\n                          resizeTo(\n                            at,\n                            startWidth + (moveEvent.clientX - startX),\n                            column\n                          )\n                        }\n\n                        const done = () => {\n                          window.removeEventListener(\"pointermove\", move)\n                          window.removeEventListener(\"pointerup\", done)\n                          const width =\n                            cols.current[at]?.getBoundingClientRect().width\n                          if (width) onColumnResize?.(column.key, width)\n                        }\n\n                        window.addEventListener(\"pointermove\", move)\n                        window.addEventListener(\"pointerup\", done)\n                      }}\n                      onKeyDown={(event) => {\n                        const step =\n                          event.key === \"ArrowLeft\"\n                            ? -KEY_STEP\n                            : event.key === \"ArrowRight\"\n                              ? KEY_STEP\n                              : 0\n\n                        if (step === 0) return\n                        event.preventDefault()\n                        freezeWidths()\n\n                        const current =\n                          cols.current[at]?.getBoundingClientRect().width ?? 0\n                        const width = resizeTo(at, current + step, column)\n                        if (width) onColumnResize?.(column.key, width)\n                      }}\n                    >\n                      <span\n                        aria-hidden=\"true\"\n                        className=\"bg-border group-hover/resize:bg-ring group-focus-visible/resize:bg-ring absolute inset-y-1 w-px transition-colors duration-150 motion-reduce:transition-none\"\n                      />\n                      {/* A one pixel line cannot be hit, so the target is wider\n                          than the line it moves. */}\n                      <span\n                        aria-hidden=\"true\"\n                        className=\"absolute inset-y-0 -right-2 -left-2\"\n                      />\n                    </span>\n                  )}\n                </th>\n              )\n            })}\n          </tr>\n        </thead>\n\n        <tbody>\n          {skeleton ? (\n            // Shaped like the rows it stands in for, and the same height, so\n            // nothing shifts underneath the reader when the data lands.\n            Array.from({ length: Math.max(1, loadingRows) }, (_, row) => (\n              <tr\n                key={`placeholder-${row}`}\n                className=\"border-border/60 border-b last:border-0\"\n              >\n                {selectable && (\n                  <td className={cellPadding}>\n                    <span className=\"bg-muted block size-4 animate-pulse rounded-sm motion-reduce:animate-none\" />\n                  </td>\n                )}\n                {columns.map((column, index) => (\n                  <td\n                    key={column.key}\n                    className={cn(cellPadding, column.cellClassName)}\n                  >\n                    <span\n                      className={cn(\n                        \"bg-muted block h-4 animate-pulse rounded-sm motion-reduce:animate-none\",\n                        column.align === \"end\" && \"ml-auto\"\n                      )}\n                      style={{\n                        width: `${58 + ((row * 7 + index * 13) % 34)}%`,\n                      }}\n                    />\n                  </td>\n                ))}\n              </tr>\n            ))\n          ) : ordered.length === 0 ? (\n            <tr>\n              <td\n                colSpan={columns.length + offset}\n                className=\"text-muted-foreground px-4 py-8 text-sm\"\n              >\n                {empty}\n              </td>\n            </tr>\n          ) : (\n            ordered.map((row, index) => {\n              const key = keys[index]!\n              const isPicked = chosen.has(key)\n\n              return (\n                <tr\n                  key={key}\n                  data-selected={isPicked ? \"\" : undefined}\n                  onClick={\n                    onRowClick ? () => onRowClick(row, index) : undefined\n                  }\n                  className={cn(\n                    \"border-border/60 bg-card border-b transition-colors duration-100 last:border-0 motion-reduce:transition-none\",\n                    striped && SURFACE.stripe,\n                    (onRowClick || selectable) && SURFACE.hover,\n                    onRowClick && \"cursor-pointer\",\n                    isPicked && [\n                      SURFACE.picked,\n                      SURFACE.pickedHover,\n                      \"shadow-[inset_2px_0_0_var(--primary)]\",\n                    ],\n                    rowClassName?.(row, index)\n                  )}\n                >\n                  {selectable && (\n                    <td\n                      style={pinStyle(0)}\n                      className={cn(\n                        cellPadding,\n                        \"align-middle\",\n                        pinned.includes(0) && \"bg-inherit\",\n                        pinClass(0, \"z-10\")\n                      )}\n                    >\n                      <input\n                        type=\"checkbox\"\n                        className=\"accent-primary focus-visible:ring-ring size-4 align-middle focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none\"\n                        checked={isPicked}\n                        aria-label={`Select ${getLabel?.(row) ?? `row ${index + 1}`}`}\n                        onClick={(event) => event.stopPropagation()}\n                        onChange={(event) =>\n                          toggleRow(\n                            index,\n                            key,\n                            (event.nativeEvent as PointerEvent).shiftKey\n                          )\n                        }\n                      />\n                    </td>\n                  )}\n\n                  {columns.map((column, position) => (\n                    <td\n                      key={column.key}\n                      style={pinStyle(position + offset)}\n                      className={cn(\n                        cellPadding,\n                        \"align-middle\",\n                        ALIGN[column.align ?? \"start\"],\n                        pinned.includes(position + offset) && \"bg-inherit\",\n                        pinClass(position + offset, \"z-10\"),\n                        column.cellClassName\n                      )}\n                    >\n                      <div\n                        className={\n                          column.wrap\n                            ? \"break-words whitespace-normal\"\n                            : \"truncate\"\n                        }\n                      >\n                        {column.cell\n                          ? column.cell(row, index)\n                          : String(\n                              (row as Record<string, unknown>)[column.key] ?? \"\"\n                            )}\n                      </div>\n                    </td>\n                  ))}\n                </tr>\n              )\n            })\n          )}\n        </tbody>\n\n        {columns.some((column) => column.footer !== undefined) && (\n          <tfoot className={SURFACE.foot}>\n            <tr>\n              {selectable && (\n                <td\n                  style={pinStyle(0)}\n                  className={cn(\n                    \"border-border border-t\",\n                    SURFACE.foot,\n                    cellPadding,\n                    pinClass(0, \"z-10\")\n                  )}\n                />\n              )}\n              {columns.map((column, position) => (\n                <td\n                  key={column.key}\n                  style={pinStyle(position + offset)}\n                  className={cn(\n                    \"border-border border-t text-xs font-semibold\",\n                    SURFACE.foot,\n                    cellPadding,\n                    ALIGN[column.align ?? \"start\"],\n                    pinClass(position + offset, \"z-10\")\n                  )}\n                >\n                  {skeleton ? (\n                    // A total drawn from rows that are still arriving would be\n                    // the previous answer sitting over the new one.\n                    <span\n                      className={cn(\n                        \"bg-muted block h-3 w-12 animate-pulse rounded-sm motion-reduce:animate-none\",\n                        column.align === \"end\" && \"ml-auto\"\n                      )}\n                    />\n                  ) : typeof column.footer === \"function\" ? (\n                    column.footer(ordered)\n                  ) : (\n                    column.footer\n                  )}\n                </td>\n              ))}\n            </tr>\n          </tfoot>\n        )}\n      </table>\n\n      {selectable && (\n        <span\n          className=\"sr-only\"\n          role=\"status\"\n          aria-live=\"polite\"\n          id={`${reactId}-selection`}\n        >\n          {picked.length} of {keys.length} selected\n        </span>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "table",
    "data",
    "sort",
    "selection",
    "layout"
  ],
  "type": "registry:ui"
}