{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tree-table",
  "title": "Tree Table",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/tree-table/TreeTable.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { ChevronRight, FileBox } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Checkbox } from '@/components/ui/checkbox'\nimport { Spinner } from '@/components/ui/spinner'\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'\nimport type { TreeTableColumn, TreeTableRow } from './types'\n\nexport interface TreeTableProps {\n  /** Tree-structured row data. */\n  data: TreeTableRow[]\n  /** Column configuration. */\n  columns: TreeTableColumn[]\n  /** Indent per nesting level in pixels. Default 24. */\n  indent?: number\n  /** Expand all rows on mount. Default false. */\n  defaultExpanded?: boolean\n  /** Show row selection checkboxes. Default false. */\n  selectable?: boolean\n  /** Loading state — shows a spinner overlay. Default false. */\n  loading?: boolean\n  /** Empty state message. Default 'No data.'. */\n  emptyText?: string\n  /** Controlled selected row ids (v-model:selected parity). */\n  selected?: string[]\n  /** Called when the selected set changes. */\n  onSelectedChange?: (ids: string[]) => void\n  /** Called when the selected set changes (alias of onSelectedChange). */\n  onSelect?: (ids: string[]) => void\n  /** Called when a row is expanded or collapsed. */\n  onExpand?: (id: string, expanded: boolean) => void\n  /** Replace the default expand/collapse chevron. */\n  expandIcon?: (expanded: boolean) => React.ReactNode\n  /** Per-cell renderer keyed by column — mirrors the Vue `cell-<key>` slot. */\n  renderCell?: (col: TreeTableColumn, row: TreeTableRow, depth: number) => React.ReactNode\n  className?: string\n}\n\ninterface FlatRow {\n  row: TreeTableRow\n  depth: number\n  hasChildren: boolean\n}\n\nconst TreeTable = React.forwardRef<HTMLDivElement, TreeTableProps>(\n  (\n    {\n      data,\n      columns,\n      indent = 24,\n      defaultExpanded = false,\n      selectable = false,\n      loading = false,\n      emptyText = 'No data.',\n      selected,\n      onSelectedChange,\n      onSelect,\n      onExpand,\n      expandIcon,\n      renderCell,\n      className,\n    },\n    ref,\n  ) => {\n    const [expandedSet, setExpandedSet] = React.useState<Set<string>>(new Set())\n    const [internalSelected, setInternalSelected] = React.useState<Set<string>>(new Set())\n\n    // Controlled `selected` wins when provided; otherwise fall back to internal state.\n    const selectedSet = React.useMemo(\n      () => (selected ? new Set(selected) : internalSelected),\n      [selected, internalSelected],\n    )\n\n    const expandAll = React.useCallback(() => {\n      const next = new Set<string>()\n      const walk = (rows: TreeTableRow[]) => {\n        for (const row of rows) {\n          if (row.children?.length) {\n            next.add(row.id)\n            walk(row.children)\n          }\n        }\n      }\n      walk(data)\n      setExpandedSet(next)\n    }, [data])\n\n    // Expand all on mount (and when data identity changes) if requested.\n    React.useEffect(() => {\n      if (defaultExpanded) expandAll()\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [defaultExpanded])\n\n    React.useEffect(() => {\n      if (defaultExpanded) expandAll()\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [data])\n\n    const flatRows = React.useMemo<FlatRow[]>(() => {\n      const out: FlatRow[] = []\n      const walk = (rows: TreeTableRow[], depth: number) => {\n        for (const row of rows) {\n          const hasChildren = !!row.children?.length\n          out.push({ row, depth, hasChildren })\n          if (hasChildren && expandedSet.has(row.id)) {\n            walk(row.children, depth + 1)\n          }\n        }\n      }\n      walk(data, 0)\n      return out\n    }, [data, expandedSet])\n\n    const isExpanded = (id: string) => expandedSet.has(id)\n    const isSelected = (id: string) => selectedSet.has(id)\n\n    const toggleExpand = (row: TreeTableRow) => {\n      setExpandedSet((prev) => {\n        const next = new Set(prev)\n        if (next.has(row.id)) next.delete(row.id)\n        else next.add(row.id)\n        onExpand?.(row.id, next.has(row.id))\n        return next\n      })\n    }\n\n    const toggleSelect = (row: TreeTableRow) => {\n      const next = new Set(selectedSet)\n      if (next.has(row.id)) next.delete(row.id)\n      else next.add(row.id)\n      const ids = [...next]\n      if (selected === undefined) setInternalSelected(next)\n      onSelectedChange?.(ids)\n      onSelect?.(ids)\n    }\n\n    const isEmpty = flatRows.length === 0\n\n    return (\n      <div ref={ref} data-uipkge data-slot=\"tree-table\" className={cn('relative w-full', className)}>\n        <Table>\n          <TableHeader>\n            <TableRow>\n              {selectable && (\n                <TableHead className=\"w-10\">\n                  <span className=\"sr-only\">Select</span>\n                </TableHead>\n              )}\n              {columns.map((col) => (\n                <TableHead key={col.key} className={cn(col.headerClass)}>\n                  {col.label}\n                </TableHead>\n              ))}\n            </TableRow>\n          </TableHeader>\n          <TableBody>\n            {flatRows.map((fr) => (\n              <TableRow\n                key={fr.row.id}\n                data-depth={fr.depth}\n                data-expanded={fr.hasChildren ? isExpanded(fr.row.id) : undefined}\n                data-selected={isSelected(fr.row.id) ? '' : undefined}\n              >\n                {selectable && (\n                  <TableCell className=\"w-10\">\n                    <Checkbox checked={isSelected(fr.row.id)} onCheckedChange={() => toggleSelect(fr.row)} />\n                  </TableCell>\n                )}\n\n                {columns.map((col, ci) => (\n                  <TableCell key={col.key} className={cn(ci === 0 && 'font-medium', col.cellClass)}>\n                    <div\n                      className=\"flex items-center\"\n                      style={ci === 0 ? { paddingLeft: `${fr.depth * indent}px` } : undefined}\n                    >\n                      {ci === 0 && fr.hasChildren && (\n                        <button\n                          type=\"button\"\n                          className=\"text-muted-foreground hover:bg-muted hover:text-foreground mr-1.5 flex size-5 shrink-0 items-center justify-center rounded\"\n                          aria-label={isExpanded(fr.row.id) ? 'Collapse' : 'Expand'}\n                          aria-expanded={isExpanded(fr.row.id)}\n                          onClick={() => toggleExpand(fr.row)}\n                        >\n                          {expandIcon ? (\n                            expandIcon(isExpanded(fr.row.id))\n                          ) : (\n                            <ChevronRight\n                              className={cn(\n                                'size-4 transition-transform duration-150',\n                                isExpanded(fr.row.id) && 'rotate-90',\n                              )}\n                            />\n                          )}\n                        </button>\n                      )}\n                      {ci === 0 && !fr.hasChildren && <span className=\"mr-1.5 w-5 shrink-0\" />}\n\n                      {renderCell\n                        ? (renderCell(col, fr.row, fr.depth) ??\n                          (col.render ? col.render(fr.row) : fr.row[col.key]))\n                        : col.render\n                          ? col.render(fr.row)\n                          : fr.row[col.key]}\n                    </div>\n                  </TableCell>\n                ))}\n              </TableRow>\n            ))}\n\n            {isEmpty && !loading && (\n              <TableRow>\n                <TableCell colSpan={columns.length + (selectable ? 1 : 0)} className=\"h-24 text-center\">\n                  <div className=\"text-muted-foreground flex flex-col items-center gap-2\">\n                    <FileBox className=\"size-8\" />\n                    <span className=\"text-sm\">{emptyText}</span>\n                  </div>\n                </TableCell>\n              </TableRow>\n            )}\n          </TableBody>\n        </Table>\n\n        {loading && (\n          <div className=\"bg-background/60 absolute inset-0 flex items-center justify-center backdrop-blur-sm\">\n            <Spinner size=\"lg\" />\n          </div>\n        )}\n      </div>\n    )\n  },\n)\nTreeTable.displayName = 'TreeTable'\n\nexport { TreeTable }\nexport type { TreeTableColumn, TreeTableRow } from './types'\n",
      "type": "registry:ui",
      "target": "~/components/ui/tree-table/TreeTable.tsx"
    },
    {
      "path": "packages/registry-react/components/tree-table/types.ts",
      "content": "import type { ReactNode } from 'react'\n\nexport interface TreeTableColumn<T = any> {\n  /** Unique key matching a field on the row data. */\n  key: string\n  /** Header label. */\n  label: string\n  /** Optional class for the header cell. */\n  headerClass?: string\n  /** Optional class for body cells in this column. */\n  cellClass?: string\n  /** Custom cell renderer: receives the row and returns a React node. */\n  render?: (row: T) => ReactNode\n}\n\nexport interface TreeTableRow<T = any> {\n  /** Unique id for the row. */\n  id: string\n  /** Row data fields keyed by column key. */\n  [key: string]: any\n  /** Child rows. */\n  children?: TreeTableRow<T>[]\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/tree-table/types.ts"
    },
    {
      "path": "packages/registry-react/components/tree-table/index.ts",
      "content": "export { TreeTable, type TreeTableProps } from './TreeTable'\nexport type { TreeTableColumn, TreeTableRow } from './types'\n",
      "type": "registry:ui",
      "target": "~/components/ui/tree-table/index.ts"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/table.json",
    "https://uipkge.dev/r/react/checkbox.json",
    "https://uipkge.dev/r/react/spinner.json"
  ],
  "description": "Hierarchical data table with expandable parent/child rows. Supports tree-structured data, column configuration, expand/collapse with per-level indent, row selection checkboxes, a loading overlay, and an empty state. Built on the existing table primitives.",
  "categories": [
    "data",
    "display"
  ]
}