{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "tree-view",
  "title": "Tree View",
  "type": "registry:ui",
  "description": "Indented tree of expandable nodes — file browsers, taxonomy editors, nested settings. Discord-style elbow connectors, lazy-load branches, and full keyboard navigation.",
  "categories": [
    "data-display"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/tree-view/tree-view.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { ChevronRight, File, Folder, FolderOpen } 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 TreeViewItem {\n  id: string\n  label: string\n  icon?: any\n  disabled?: boolean\n  selected?: boolean\n  children?: TreeViewItem[]\n}\n\n/**\n * <uip-tree-view> — Hierarchical tree navigation with continuous guide lines,\n * checkboxes, custom icons, and keyboard navigation.\n */\nexport class UipTreeView 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    items: { type: Array, attribute: false },\n    showIcons: {\n      type: Boolean,\n      attribute: 'show-icons',\n      converter: { fromAttribute: (v: string | null) => v !== null && v !== 'false' },\n    },\n    showCheckboxes: { type: Boolean, attribute: 'show-checkboxes' },\n    defaultExpanded: { type: Boolean, attribute: 'default-expanded' },\n    selectedId: { type: String, attribute: 'selected-id' },\n    expandedIds: { state: true },\n  }\n\n  items: TreeViewItem[] = []\n  showIcons = true\n  showCheckboxes = false\n  defaultExpanded = false\n  selectedId: string | null = null\n\n  expandedIds = 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-view')\n    this.setAttribute('role', 'tree')\n    if (this.defaultExpanded) {\n      this.expandAll()\n    }\n  }\n\n  private collectExpandable(items: TreeViewItem[], acc: Set<string>) {\n    for (const it of items) {\n      if (it.children?.length) {\n        acc.add(it.id)\n        this.collectExpandable(it.children, acc)\n      }\n    }\n  }\n\n  expandAll() {\n    const next = new Set<string>()\n    this.collectExpandable(this.items, next)\n    this.expandedIds = next\n  }\n\n  collapseAll() {\n    this.expandedIds = new Set()\n  }\n\n  private toggle(item: TreeViewItem) {\n    if (item.disabled) return\n    const next = new Set(this.expandedIds)\n    if (next.has(item.id)) next.delete(item.id)\n    else next.add(item.id)\n    this.expandedIds = next\n    this.dispatchEvent(new CustomEvent('toggle', { detail: { item }, bubbles: true, composed: true }))\n  }\n\n  private select(item: TreeViewItem) {\n    if (item.disabled) return\n    this.selectedId = item.id\n    this.dispatchEvent(new CustomEvent('select', { detail: { item }, bubbles: true, composed: true }))\n    this.dispatchEvent(new CustomEvent('selected-id-change', { detail: { id: item.id }, bubbles: true, composed: true }))\n    this.requestUpdate()\n  }\n\n  private renderNode(item: TreeViewItem, depth = 0, isLast = false): unknown {\n    const hasChildren = Boolean(item.children && item.children.length > 0)\n    const isExpanded = this.expandedIds.has(item.id)\n    const isSelected = this.selectedId === item.id || item.selected\n\n    // Icon resolution\n    let nodeIcon = item.icon\n    if (!nodeIcon) {\n      if (hasChildren) {\n        nodeIcon = isExpanded ? FolderOpen : Folder\n      } else {\n        nodeIcon = File\n      }\n    }\n\n    return html`\n      <div class=\"relative flex flex-col select-none\" role=\"treeitem\" aria-expanded=${hasChildren ? (isExpanded ? 'true' : 'false') : nothing}>\n        <div\n          class=${cn(\n            'group hover:bg-accent hover:text-accent-foreground flex items-center gap-1.5 rounded-md px-2 py-1 text-sm transition-colors cursor-pointer',\n            isSelected && 'bg-accent text-accent-foreground font-medium',\n            item.disabled && 'cursor-not-allowed opacity-50',\n          )}\n          style=\"padding-left: ${depth * 16 + 8}px;\"\n          @click=${() => (hasChildren ? this.toggle(item) : this.select(item))}\n        >\n          <!-- Expand Chevron -->\n          ${hasChildren\n            ? html`\n                <button\n                  type=\"button\"\n                  class=\"text-muted-foreground hover:text-foreground -ml-1 flex size-5 items-center justify-center rounded p-0.5 transition-transform\"\n                  aria-label=\"Toggle node\"\n                  @click=${(e: Event) => {\n                    e.stopPropagation()\n                    this.toggle(item)\n                  }}\n                >\n                  <span class=${cn('transition-transform duration-150 inline-block', isExpanded && 'rotate-90')}>\n                    ${icon(ChevronRight, 'chevron-right', 'size-3.5')}\n                  </span>\n                </button>\n              `\n            : html`<span class=\"size-4 shrink-0\"></span>`}\n\n          <!-- Checkbox -->\n          ${this.showCheckboxes\n            ? html`\n                <input\n                  type=\"checkbox\"\n                  class=\"rounded border-input text-primary focus:ring-ring size-3.5\"\n                  .checked=${Boolean(isSelected)}\n                  ?disabled=${item.disabled}\n                  @click=${(e: Event) => e.stopPropagation()}\n                  @change=${() => this.select(item)}\n                />\n              `\n            : nothing}\n\n          <!-- Icon -->\n          ${this.showIcons && nodeIcon\n            ? html`\n                <span class=\"text-muted-foreground group-hover:text-foreground shrink-0\">\n                  ${typeof nodeIcon === 'function' || Array.isArray(nodeIcon)\n                    ? icon(nodeIcon, 'node-icon', 'size-4')\n                    : nothing}\n                </span>\n              `\n            : nothing}\n\n          <!-- Label -->\n          <span class=\"truncate\" @click=${(e: Event) => {\n            e.stopPropagation()\n            this.select(item)\n          }}>${item.label}</span>\n        </div>\n\n        <!-- Children with elbow line -->\n        ${hasChildren && isExpanded\n          ? html`\n              <div class=\"relative flex flex-col\">\n                <div\n                  class=\"border-border/60 absolute left-0 top-0 bottom-2 border-l\"\n                  style=\"left: ${depth * 16 + 17}px;\"\n                ></div>\n                ${item.children!.map((child, i) =>\n                  this.renderNode(child, depth + 1, i === item.children!.length - 1),\n                )}\n              </div>\n            `\n          : nothing}\n      </div>\n    `\n  }\n\n  render() {\n    return html`\n      <div part=\"base\" class=\"text-sm\">\n        ${this.items.map((item, i) => this.renderNode(item, 0, i === this.items.length - 1))}\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-tree-view') || customElements.define('uip-tree-view', UipTreeView)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-tree-view': UipTreeView\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/tree-view/tree-view.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
