{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "opentui/table",
  "title": "Table",
  "description": "Sortable, selectable, paginated data table",
  "dependencies": [
    "@opentui/react"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/opentui/use-theme.json"
  ],
  "files": [
    {
      "path": "registry/opentui/ui/table.tsx",
      "content": "/* @jsxImportSource @opentui/react */\nimport { useKeyboard } from \"@opentui/react\";\nimport type { Key } from \"react\";\nimport { useMemo, useState } from \"react\";\n\nimport { useTheme } from \"@/hooks/use-theme\";\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> {\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}\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 pad = (\n  str: string,\n  width: number,\n  align: \"left\" | \"right\" | \"center\" = \"left\"\n): string => {\n  const s = String(str);\n  if (s.length >= width) {\n    return s.slice(0, width);\n  }\n  const diff = width - s.length;\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 s + \" \".repeat(diff);\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 fg={color}>{skeleton.left}</text>\n    {intersperse(\n      widths.map((w, i) => (\n        <text key={i} fg={color}>\n          {skeleton.line.repeat(w + 2)}\n        </text>\n      )),\n      (i) => (\n        <text key={`sep-${i}`} fg={color}>\n          {skeleton.cross}\n        </text>\n      )\n    )}\n    <text fg={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  key?: Key | null;\n  widths: number[];\n  cells: {\n    text: string;\n    align: \"left\" | \"right\" | \"center\";\n  }[];\n  skeleton: SkeletonChars;\n  borderColor: string;\n  textColor: string;\n  bold?: boolean;\n  inverse?: boolean;\n}) => (\n  <box flexDirection=\"row\">\n    <text fg={borderColor}>{skeleton.left}</text>\n    {intersperse(\n      cells.map((cell, i) => {\n        const content = ` ${pad(cell.text, widths[i] ?? 0, cell.align)} `;\n        return (\n          <text\n            key={i}\n            fg={inverse ? undefined : textColor}\n            bg={inverse ? textColor : undefined}\n          >\n            {bold ? <b>{content}</b> : content}\n          </text>\n        );\n      }),\n      (i) => (\n        <text key={`sep-${i}`} fg={borderColor}>\n          {skeleton.cross}\n        </text>\n      )\n    )}\n    <text fg={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}: TableProps<T>) => {\n  const theme = useTheme();\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  useKeyboard((key) => {\n    if (key.name === \"up\") {\n      setActiveRow((r) => Math.max(0, r - 1));\n    } else if (key.name === \"down\") {\n      setActiveRow((r) => Math.min(visible.length - 1, r + 1));\n    } else if (key.name === \"return\" && selectable) {\n      onSelect?.(visible[activeRow] as T);\n    } else if (sortable && key.name === \"left\") {\n      setSortColIdx((i) => Math.max(0, i - 1));\n    } else if (sortable && key.name === \"right\") {\n      setSortColIdx((i) => Math.min(columns.length - 1, i + 1));\n    } else if (sortable && key.name === \"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\n  const colWidths = columns.map((col) => {\n    let dataMax = 0;\n    for (const row of data) {\n      dataMax = Math.max(dataMax, String(row[col.key] ?? \"\").length);\n    }\n    return col.width ?? Math.max(col.header.length, 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 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={true}\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={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 = `… ${data.length - maxRows} more rows`;\n            return (\n              <box flexDirection=\"row\">\n                <text fg={resolvedBorderColor}>│</text>\n                <text\n                  fg={theme.colors.mutedForeground}\n                >{` ${pad(msg, innerWidth)} `}</text>\n                <text fg={resolvedBorderColor}>│</text>\n              </box>\n            );\n          })()\n        : null}\n      <SkeletonRow\n        widths={colWidths}\n        skeleton={BORDER.bottom}\n        color={resolvedBorderColor}\n      />\n    </box>\n  );\n};\n",
      "type": "registry:ui",
      "target": "components/ui/table.tsx"
    }
  ],
  "categories": [
    "data"
  ],
  "type": "registry:ui"
}