{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "tree-table",
  "title": "Tree Table",
  "type": "registry:ui",
  "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"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/tree-table/tree-table.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { ChevronRight, Loader2 } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { icon } from '../../lib/icon'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\n\nexport interface TreeTableRow {\n  id: string\n  children?: TreeTableRow[]\n  [key: string]: any\n}\n\nexport interface TreeTableColumn<T = any> {\n  key: string\n  label: string\n  headerClass?: string\n  cellClass?: string\n}\n\ninterface FlatRow {\n  row: TreeTableRow\n  depth: number\n  hasChildren: boolean\n  parentId: string | null\n}\n\n/**\n * <uip-tree-table> — Hierarchical data grid with expandable nested rows,\n * column definitions, multi-row selection, and loading states.\n */\nexport class UipTreeTable extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n        width: 100%;\n      }\n    `,\n  ]\n\n  static properties = {\n    data: { type: Array, attribute: false },\n    columns: { type: Array, attribute: false },\n    indent: { type: Number },\n    defaultExpanded: { type: Boolean, attribute: 'default-expanded' },\n    selectable: { type: Boolean },\n    loading: { type: Boolean },\n    emptyText: { type: String, attribute: 'empty-text' },\n    selected: { type: Array, attribute: false },\n    renderCell: { attribute: false },\n    expandIcon: { attribute: false },\n    expandedSet: { state: true },\n    internalSelected: { state: true },\n  }\n\n  data: TreeTableRow[] = []\n  columns: TreeTableColumn[] = []\n  indent = 24\n  defaultExpanded = false\n  selectable = false\n  loading = false\n  emptyText = 'No data.'\n  selected?: string[]\n  renderCell?: (col: TreeTableColumn, row: TreeTableRow, depth: number) => unknown\n  expandIcon?: (expanded: boolean) => unknown\n\n  expandedSet = new Set<string>()\n  internalSelected = new Set<string>()\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'tree-table')\n    if (this.defaultExpanded) {\n      this.expandAll()\n    }\n  }\n\n  updated(changedProperties: Map<string, unknown>) {\n    if (changedProperties.has('defaultExpanded') || changedProperties.has('data')) {\n      if (this.defaultExpanded) {\n        this.expandAll()\n      }\n    }\n  }\n\n  private expandAll() {\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(this.data)\n    this.expandedSet = next\n  }\n\n  private toggle(id: string) {\n    const next = new Set(this.expandedSet)\n    const expanded = !next.has(id)\n    if (expanded) next.add(id)\n    else next.delete(id)\n    this.expandedSet = next\n    this.dispatchEvent(\n      new CustomEvent('expand', {\n        detail: { id, expanded },\n        bubbles: true,\n        composed: true,\n      }),\n    )\n  }\n\n  private toggleSelect(id: string) {\n    const current = this.selected ? new Set(this.selected) : new Set(this.internalSelected)\n    if (current.has(id)) current.delete(id)\n    else current.add(id)\n    this.internalSelected = current\n    const ids = Array.from(current)\n    this.dispatchEvent(\n      new CustomEvent('selected-change', {\n        detail: { ids },\n        bubbles: true,\n        composed: true,\n      }),\n    )\n  }\n\n  private toggleSelectAll(flatRows: FlatRow[]) {\n    const current = this.selected ? new Set(this.selected) : new Set(this.internalSelected)\n    const allSelected = flatRows.length > 0 && flatRows.every((f) => current.has(f.row.id))\n    const next = new Set<string>()\n    if (!allSelected) {\n      flatRows.forEach((f) => next.add(f.row.id))\n    }\n    this.internalSelected = next\n    const ids = Array.from(next)\n    this.dispatchEvent(\n      new CustomEvent('selected-change', {\n        detail: { ids },\n        bubbles: true,\n        composed: true,\n      }),\n    )\n  }\n\n  private flattenRows(): FlatRow[] {\n    const result: FlatRow[] = []\n    const walk = (rows: TreeTableRow[], depth: number, parentId: string | null) => {\n      for (const row of rows) {\n        const hasChildren = Boolean(row.children && row.children.length > 0)\n        result.push({ row, depth, hasChildren, parentId })\n        if (hasChildren && this.expandedSet.has(row.id)) {\n          walk(row.children!, depth + 1, row.id)\n        }\n      }\n    }\n    walk(this.data, 0, null)\n    return result\n  }\n\n  render() {\n    const flatRows = this.flattenRows()\n    const selectedSet = this.selected ? new Set(this.selected) : this.internalSelected\n    const isAllSelected = flatRows.length > 0 && flatRows.every((f) => selectedSet.has(f.row.id))\n    const isSomeSelected = flatRows.some((f) => selectedSet.has(f.row.id)) && !isAllSelected\n\n    return html`\n      <div part=\"base\" class=\"relative w-full overflow-hidden rounded-lg border border-border bg-card\">\n        ${this.loading\n          ? html`\n              <div\n                class=\"bg-background/80 absolute inset-0 z-20 flex items-center justify-center backdrop-blur-xs\"\n              >\n                ${icon(Loader2, 'loader-2', 'size-6 animate-spin text-primary')}\n              </div>\n            `\n          : nothing}\n\n        <div class=\"overflow-x-auto\">\n          <table class=\"w-full text-left text-sm border-collapse\">\n            <thead class=\"border-b border-border bg-muted/50 text-muted-foreground text-xs uppercase font-medium\">\n              <tr>\n                ${this.selectable\n                  ? html`\n                      <th class=\"w-10 px-3 py-3 text-center\">\n                        <input\n                          type=\"checkbox\"\n                          class=\"rounded border-input text-primary focus:ring-ring size-4\"\n                          .checked=${isAllSelected}\n                          .indeterminate=${isSomeSelected}\n                          @change=${() => this.toggleSelectAll(flatRows)}\n                        />\n                      </th>\n                    `\n                  : nothing}\n                ${this.columns.map(\n                  (col) => html`<th class=${cn('px-4 py-3 font-semibold', col.headerClass)}>${col.label}</th>`,\n                )}\n              </tr>\n            </thead>\n            <tbody class=\"divide-y divide-border\">\n              ${flatRows.length === 0\n                ? html`\n                    <tr>\n                      <td\n                        colspan=${this.columns.length + (this.selectable ? 1 : 0)}\n                        class=\"text-muted-foreground px-4 py-8 text-center\"\n                      >\n                        ${this.emptyText}\n                      </td>\n                    </tr>\n                  `\n                : flatRows.map(({ row, depth, hasChildren }) => {\n                    const isRowSelected = selectedSet.has(row.id)\n                    const isExpanded = this.expandedSet.has(row.id)\n\n                    return html`\n                      <tr\n                        class=${cn(\n                          'hover:bg-muted/50 transition-colors',\n                          isRowSelected && 'bg-primary/5',\n                        )}\n                      >\n                        ${this.selectable\n                          ? html`\n                              <td class=\"w-10 px-3 py-2 text-center\">\n                                <input\n                                  type=\"checkbox\"\n                                  class=\"rounded border-input text-primary focus:ring-ring size-4\"\n                                  .checked=${isRowSelected}\n                                  @change=${() => this.toggleSelect(row.id)}\n                                />\n                              </td>\n                            `\n                          : nothing}\n                        ${this.columns.map((col, colIdx) => {\n                          const customCell = this.renderCell ? this.renderCell(col, row, depth) : undefined\n\n                          if (colIdx === 0) {\n                            return html`\n                              <td class=${cn('px-4 py-2 font-medium', col.cellClass)}>\n                                <div class=\"flex items-center gap-1.5\" style=\"padding-left: ${depth * this.indent}px;\">\n                                  ${hasChildren\n                                    ? html`\n                                        <button\n                                          type=\"button\"\n                                          class=\"text-muted-foreground hover:text-foreground flex size-5 items-center justify-center rounded p-0.5\"\n                                          @click=${() => this.toggle(row.id)}\n                                        >\n                                          ${this.expandIcon\n                                            ? this.expandIcon(isExpanded)\n                                            : html`\n                                                <span\n                                                  class=${cn(\n                                                    'inline-block transition-transform duration-150',\n                                                    isExpanded && 'rotate-90',\n                                                  )}\n                                                >\n                                                  ${icon(ChevronRight, 'chevron-right', 'size-3.5')}\n                                                </span>\n                                              `}\n                                        </button>\n                                      `\n                                    : html`<span class=\"size-5 shrink-0\"></span>`}\n                                  ${customCell ?? row[col.key]}\n                                </div>\n                              </td>\n                            `\n                          }\n\n                          return html`\n                            <td class=${cn('px-4 py-2', col.cellClass)}>\n                              ${customCell ?? row[col.key]}\n                            </td>\n                          `\n                        })}\n                      </tr>\n                    `\n                  })}\n            </tbody>\n          </table>\n        </div>\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-tree-table') || customElements.define('uip-tree-table', UipTreeTable)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-tree-table': UipTreeTable\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/tree-table/tree-table.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
