{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "tree-table",
  "title": "Tree Table",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/tree-table/TreeTable.vue",
      "content": "<script setup lang=\"ts\" generic=\"T extends TreeTableRow\">\nimport { computed, onMounted, ref, watch, type HTMLAttributes } from 'vue'\nimport { ChevronRight, FileBox } from 'lucide-vue-next'\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\ninterface Props {\n  /** Tree-structured row data. */\n  data: TreeTableRow<T>[]\n  /** Column configuration. */\n  columns: TreeTableColumn<T>[]\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  class?: HTMLAttributes['class']\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  indent: 24,\n  defaultExpanded: false,\n  selectable: false,\n  loading: false,\n  emptyText: 'No data.',\n})\n\nconst emit = defineEmits<{\n  'update:selected': [ids: string[]]\n  select: [ids: string[]]\n  expand: [id: string, expanded: boolean]\n}>()\n\nconst expanded = ref<Set<string>>(new Set())\nconst selected = ref<Set<string>>(new Set())\n\ninterface FlatRow {\n  row: TreeTableRow<T>\n  depth: number\n  hasChildren: boolean\n}\n\nconst flatRows = computed<FlatRow[]>(() => {\n  const out: FlatRow[] = []\n  const walk = (rows: TreeTableRow<T>[], depth: number) => {\n    for (const row of rows) {\n      const hasChildren = !!row.children?.length\n      out.push({ row, depth, hasChildren })\n      if (hasChildren && expanded.value.has(row.id)) {\n        walk(row.children as TreeTableRow<T>[], depth + 1)\n      }\n    }\n  }\n  walk(props.data, 0)\n  return out\n})\n\nfunction toggleExpand(row: TreeTableRow<T>) {\n  const next = new Set(expanded.value)\n  if (next.has(row.id)) next.delete(row.id)\n  else next.add(row.id)\n  expanded.value = next\n  emit('expand', row.id, next.has(row.id))\n}\n\nfunction toggleSelect(row: TreeTableRow<T>) {\n  const next = new Set(selected.value)\n  if (next.has(row.id)) next.delete(row.id)\n  else next.add(row.id)\n  selected.value = next\n  const ids = [...next]\n  emit('update:selected', ids)\n  emit('select', ids)\n}\n\nfunction isSelected(id: string): boolean {\n  return selected.value.has(id)\n}\n\nfunction isExpanded(id: string): boolean {\n  return expanded.value.has(id)\n}\n\nfunction expandAll() {\n  const next = new Set<string>()\n  const walk = (rows: TreeTableRow<T>[]) => {\n    for (const row of rows) {\n      if (row.children?.length) {\n        next.add(row.id)\n        walk(row.children as TreeTableRow<T>[])\n      }\n    }\n  }\n  walk(props.data)\n  expanded.value = next\n}\n\nfunction collapseAll() {\n  expanded.value = new Set()\n}\n\nonMounted(() => {\n  if (props.defaultExpanded) expandAll()\n})\n\n// Reset expanded/selected state when data identity changes.\nwatch(\n  () => props.data,\n  () => {\n    if (props.defaultExpanded) expandAll()\n  },\n  { deep: false },\n)\n\nconst isEmpty = computed(() => flatRows.value.length === 0)\n</script>\n\n<template>\n  <div data-uipkge data-slot=\"tree-table\" :class=\"cn('relative w-full', props.class)\">\n    <Table>\n      <TableHeader>\n        <TableRow>\n          <TableHead v-if=\"selectable\" class=\"w-10\">\n            <span class=\"sr-only\">Select</span>\n          </TableHead>\n          <TableHead v-for=\"col in columns\" :key=\"col.key\" :class=\"cn(col.headerClass)\">\n            {{ col.label }}\n          </TableHead>\n        </TableRow>\n      </TableHeader>\n      <TableBody>\n        <TableRow\n          v-for=\"fr in flatRows\"\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          <!-- Selection checkbox -->\n          <TableCell v-if=\"selectable\" class=\"w-10\">\n            <Checkbox :model-value=\"isSelected(fr.row.id)\" @update:model-value=\"toggleSelect(fr.row)\" />\n          </TableCell>\n\n          <!-- Data cells -->\n          <TableCell v-for=\"(col, ci) in columns\" :key=\"col.key\" :class=\"cn(ci === 0 && 'font-medium', col.cellClass)\">\n            <div class=\"flex items-center\" :style=\"ci === 0 ? { paddingLeft: `${fr.depth * indent}px` } : {}\">\n              <!-- Expand toggle on the first column -->\n              <button\n                v-if=\"ci === 0 && fr.hasChildren\"\n                type=\"button\"\n                class=\"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                @click=\"toggleExpand(fr.row)\"\n              >\n                <slot name=\"expand-icon\" :expanded=\"isExpanded(fr.row.id)\">\n                  <ChevronRight\n                    class=\"size-4 transition-transform duration-150\"\n                    :class=\"isExpanded(fr.row.id) ? 'rotate-90' : ''\"\n                  />\n                </slot>\n              </button>\n              <span v-else-if=\"ci === 0\" class=\"mr-1.5 w-5 shrink-0\" />\n\n              <slot :name=\"`cell-${col.key}`\" :row=\"fr.row\" :depth=\"fr.depth\">\n                {{ col.render ? col.render(fr.row as T) : fr.row[col.key] }}\n              </slot>\n            </div>\n          </TableCell>\n        </TableRow>\n\n        <!-- Empty state -->\n        <TableRow v-if=\"isEmpty && !loading\">\n          <TableCell :colspan=\"columns.length + (selectable ? 1 : 0)\" class=\"h-24 text-center\">\n            <div class=\"text-muted-foreground flex flex-col items-center gap-2\">\n              <FileBox class=\"size-8\" />\n              <span class=\"text-sm\">{{ emptyText }}</span>\n            </div>\n          </TableCell>\n        </TableRow>\n      </TableBody>\n    </Table>\n\n    <!-- Loading overlay -->\n    <div v-if=\"loading\" class=\"bg-background/60 absolute inset-0 flex items-center justify-center backdrop-blur-sm\">\n      <Spinner size=\"lg\" />\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/tree-table/TreeTable.vue"
    },
    {
      "path": "packages/registry-vue/components/tree-table/types.ts",
      "content": "export 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 string or VNode-friendly value. */\n  render?: (row: T) => any\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": "~/app/components/ui/tree-table/types.ts"
    },
    {
      "path": "packages/registry-vue/components/tree-table/index.ts",
      "content": "export { default as TreeTable } from './TreeTable.vue'\nexport type { TreeTableColumn, TreeTableRow } from './types'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/tree-table/index.ts"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/table.json",
    "https://uipkge.dev/r/vue/checkbox.json",
    "https://uipkge.dev/r/vue/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"
  ]
}