{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/table",
  "title": "Table",
  "description": "Sortable, selectable, paginated data table",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/interaction.json",
    "https://termcn.dev/r/ink/terminal-text.json",
    "https://termcn.dev/r/ink/use-theme.json",
    "https://termcn.dev/r/ink/use-unicode.json"
  ],
  "files": [
    {
      "path": "registry/ui/table.tsx",
      "content": "import { Box, Text } from \"ink\";\nimport React, { useEffect, useMemo, useState } from \"react\";\n\nimport { useInteraction } from \"@/hooks/use-interaction\";\nimport type { InteractionProps } from \"@/hooks/use-interaction\";\nimport { useTheme } from \"@/hooks/use-theme\";\nimport { useUnicode } from \"@/hooks/use-unicode\";\nimport {\n  padToTerminalWidth,\n  terminalWidth,\n  truncateToTerminalWidth,\n} from \"@/lib/terminal-text\";\n\nexport interface Column<T = Record<string, unknown>> {\n  key: keyof T & string;\n  header: string;\n  width?: number;\n  align?: \"left\" | \"right\" | \"center\";\n}\n\nexport interface TableProps<\n  T extends Record<string, unknown> = Record<string, unknown>,\n> extends InteractionProps {\n  data: T[];\n  columns: Column<T>[];\n  sortable?: boolean;\n  selectable?: boolean;\n  onSelect?: (row: T) => void;\n  maxRows?: number;\n  borderColor?: string;\n  \"aria-label\"?: string;\n  getRowKey?: (row: T, index: number) => React.Key;\n}\n\nconst BORDER = {\n  bottom: { cross: \"┴\", left: \"╰\", line: \"─\", right: \"╯\" },\n  data: { cross: \"│\", left: \"│\", line: \" \", right: \"│\" },\n  heading: { cross: \"│\", left: \"│\", line: \" \", right: \"│\" },\n  separator: { cross: \"┼\", left: \"├\", line: \"─\", right: \"┤\" },\n  top: { cross: \"┬\", left: \"╭\", line: \"─\", right: \"╮\" },\n} as const;\n\nconst ASCII_BORDER = {\n  bottom: { cross: \"+\", left: \"+\", line: \"-\", right: \"+\" },\n  data: { cross: \"|\", left: \"|\", line: \" \", right: \"|\" },\n  heading: { cross: \"|\", left: \"|\", line: \" \", right: \"|\" },\n  separator: { cross: \"+\", left: \"+\", line: \"-\", right: \"+\" },\n  top: { cross: \"+\", left: \"+\", line: \"-\", right: \"+\" },\n} as const;\n\nconst pad = (\n  str: string,\n  width: number,\n  align: \"left\" | \"right\" | \"center\" = \"left\"\n): string => {\n  const s = truncateToTerminalWidth(String(str), width);\n  const diff = Math.max(0, width - terminalWidth(s));\n  if (align === \"right\") {\n    return \" \".repeat(diff) + s;\n  }\n  if (align === \"center\") {\n    const l = Math.floor(diff / 2);\n    return \" \".repeat(l) + s + \" \".repeat(diff - l);\n  }\n  return padToTerminalWidth(s, width);\n};\n\nconst intersperse = <T,>(items: T[], separator: (index: number) => T): T[] => {\n  const result: T[] = [];\n  for (let i = 0; i < items.length; i += 1) {\n    if (i > 0) {\n      result.push(separator(i));\n    }\n    const item = items[i];\n    if (item !== undefined) {\n      result.push(item);\n    }\n  }\n  return result;\n};\n\ninterface SkeletonChars {\n  left: string;\n  right: string;\n  cross: string;\n  line: string;\n}\n\nconst SkeletonRow = ({\n  widths,\n  skeleton,\n  color,\n}: {\n  widths: number[];\n  skeleton: SkeletonChars;\n  color: string;\n}) => (\n  <Box flexDirection=\"row\">\n    <Text color={color}>{skeleton.left}</Text>\n    {intersperse(\n      widths.map((w, i) => (\n        <Text key={i} color={color}>\n          {skeleton.line.repeat(w + 2)}\n        </Text>\n      )),\n      (i) => (\n        <Text key={`sep-${i}`} color={color}>\n          {skeleton.cross}\n        </Text>\n      )\n    )}\n    <Text color={color}>{skeleton.right}</Text>\n  </Box>\n);\n\nconst CellRow = ({\n  widths,\n  cells,\n  skeleton,\n  borderColor,\n  textColor,\n  bold,\n  inverse,\n}: {\n  widths: number[];\n  cells: { text: string; align: \"left\" | \"right\" | \"center\" }[];\n  skeleton: SkeletonChars;\n  borderColor: string;\n  textColor: string;\n  bold?: boolean;\n  inverse?: boolean;\n}) => (\n  <Box flexDirection=\"row\">\n    <Text color={borderColor}>{skeleton.left}</Text>\n    {intersperse(\n      cells.map((cell, i) => (\n        <Text key={i} color={textColor} bold={bold} inverse={inverse}>\n          {` ${pad(cell.text, widths[i] ?? 0, cell.align)} `}\n        </Text>\n      )),\n      (i) => (\n        <Text key={`sep-${i}`} color={borderColor}>\n          {skeleton.cross}\n        </Text>\n      )\n    )}\n    <Text color={borderColor}>{skeleton.right}</Text>\n  </Box>\n);\n\nexport const Table = <\n  T extends Record<string, unknown> = Record<string, unknown>,\n>({\n  data,\n  columns,\n  sortable = false,\n  selectable = false,\n  onSelect,\n  maxRows = 20,\n  borderColor,\n  id,\n  autoFocus,\n  isActive,\n  disabled,\n  \"aria-label\": ariaLabel = \"Data table\",\n  getRowKey,\n}: TableProps<T>) => {\n  const theme = useTheme();\n  const unicode = useUnicode();\n  const border = unicode ? BORDER : ASCII_BORDER;\n  const [sortKey, setSortKey] = useState<string | null>(null);\n  const [sortDir, setSortDir] = useState<\"asc\" | \"desc\">(\"asc\");\n  const [activeRow, setActiveRow] = useState(0);\n  const [sortColIdx, setSortColIdx] = useState(0);\n\n  const resolvedBorderColor = borderColor ?? theme.colors.border;\n\n  const sorted = useMemo(() => {\n    if (!sortKey) {\n      return data;\n    }\n    return [...data].toSorted((a, b) => {\n      const cmp = String(a[sortKey]).localeCompare(String(b[sortKey]));\n      return sortDir === \"asc\" ? cmp : -cmp;\n    });\n  }, [data, sortKey, sortDir]);\n\n  const visible = sorted.slice(0, maxRows);\n\n  const { isFocused } = useInteraction(\n    (input, key) => {\n      if (visible.length === 0) {\n        return;\n      }\n      if (key.upArrow) {\n        setActiveRow((r) => Math.max(0, r - 1));\n      } else if (key.downArrow) {\n        setActiveRow((r) => Math.min(visible.length - 1, r + 1));\n      } else if (key.home) {\n        setActiveRow(0);\n      } else if (key.end) {\n        setActiveRow(visible.length - 1);\n      } else if (key.return && selectable) {\n        onSelect?.(visible[activeRow] as T);\n      } else if (sortable && key.leftArrow) {\n        setSortColIdx((i) => Math.max(0, i - 1));\n      } else if (sortable && key.rightArrow) {\n        setSortColIdx((i) => Math.min(columns.length - 1, i + 1));\n      } else if (sortable && input === \"s\") {\n        const col = columns[sortColIdx];\n        if (!col) {\n          return;\n        }\n        if (sortKey === col.key) {\n          setSortDir((d) => (d === \"asc\" ? \"desc\" : \"asc\"));\n        } else {\n          setSortKey(col.key);\n          setSortDir(\"asc\");\n        }\n      }\n    },\n    { autoFocus, disabled, id, isActive }\n  );\n\n  useEffect(() => {\n    setActiveRow((row) => Math.min(row, Math.max(0, visible.length - 1)));\n  }, [visible.length]);\n\n  const colWidths = columns.map((col) => {\n    let dataMax = 0;\n    for (const row of data) {\n      dataMax = Math.max(dataMax, terminalWidth(String(row[col.key] ?? \"\")));\n    }\n    return col.width ?? Math.max(terminalWidth(col.header), dataMax);\n  });\n\n  const headerCells = columns.map((col) => ({\n    align: col.align ?? (\"left\" as const),\n    text: col.header,\n  }));\n\n  return (\n    <Box\n      flexDirection=\"column\"\n      aria-role=\"table\"\n      aria-state={{ disabled: disabled || undefined }}\n    >\n      <Text aria-label={`${ariaLabel}. ${visible.length} visible rows.`}>\n        {\"\"}\n      </Text>\n      {isFocused && selectable && visible[activeRow] && (\n        <Text\n          aria-label={`Selected row ${activeRow + 1}: ${columns\n            .map(\n              (column) =>\n                `${column.header}: ${String(visible[activeRow]?.[column.key] ?? \"\")}`\n            )\n            .join(\", \")}`}\n        >\n          {\"\"}\n        </Text>\n      )}\n      <Box aria-hidden flexDirection=\"column\">\n        <SkeletonRow\n          widths={colWidths}\n          skeleton={border.top}\n          color={resolvedBorderColor}\n        />\n        <CellRow\n          widths={colWidths}\n          cells={headerCells}\n          skeleton={border.heading}\n          borderColor={resolvedBorderColor}\n          textColor={theme.colors.primary}\n          bold\n        />\n        <SkeletonRow\n          widths={colWidths}\n          skeleton={border.separator}\n          color={resolvedBorderColor}\n        />\n        {visible.map((row, rowIdx) => {\n          const isActive = rowIdx === activeRow && selectable;\n          const rowCells = columns.map((col) => ({\n            align: col.align ?? (\"left\" as const),\n            text: String(row[col.key] ?? \"\"),\n          }));\n          return (\n            <CellRow\n              key={getRowKey?.(row, rowIdx) ?? rowIdx}\n              widths={colWidths}\n              cells={rowCells}\n              skeleton={border.data}\n              borderColor={resolvedBorderColor}\n              textColor={\n                isActive\n                  ? theme.colors.selectionForeground\n                  : theme.colors.foreground\n              }\n              inverse={isActive}\n            />\n          );\n        })}\n        {data.length > maxRows &&\n          (() => {\n            const innerWidth =\n              colWidths.reduce((a, b) => a + b, 0) + colWidths.length * 3 - 3;\n            const msg = `${unicode ? \"…\" : \"...\"} ${data.length - maxRows} more rows`;\n            return (\n              <Box flexDirection=\"row\">\n                <Text color={resolvedBorderColor}>{border.data.left}</Text>\n                <Text color={theme.colors.mutedForeground} dimColor>\n                  {` ${pad(msg, innerWidth)} `}\n                </Text>\n                <Text color={resolvedBorderColor}>{border.data.right}</Text>\n              </Box>\n            );\n          })()}\n        <SkeletonRow\n          widths={colWidths}\n          skeleton={border.bottom}\n          color={resolvedBorderColor}\n        />\n      </Box>\n      {visible.map((row, rowIdx) => (\n        <Text\n          key={`accessible-${String(getRowKey?.(row, rowIdx) ?? rowIdx)}`}\n          aria-label={`Row ${rowIdx + 1}: ${columns\n            .map(\n              (column) => `${column.header}: ${String(row[column.key] ?? \"\")}`\n            )\n            .join(\", \")}`}\n        >\n          {\"\"}\n        </Text>\n      ))}\n    </Box>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "data"
  ],
  "type": "registry:ui"
}