{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "tree-view",
  "title": "Tree View",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/tree-view/tree-view.component.ts",
      "content": "import {\n  Component,\n  EventEmitter,\n  Input,\n  OnInit,\n  OnChanges,\n  Output,\n  SimpleChanges,\n  booleanAttribute,\n  ChangeDetectionStrategy,\n} from '@angular/core'\n/** Anything that can announce to screen readers — @angular/cdk's LiveAnnouncer fits, without depending on the CDK. */\nexport interface Announcer {\n  announce(message: string): unknown\n}\nimport { cn } from '@/lib/utils'\n\nexport interface TreeViewItem {\n  id: string\n  label: string\n  icon?: string | any\n  disabled?: boolean\n  selected?: boolean\n  children?: TreeViewItem[]\n  [key: string]: unknown\n}\n\nconst INDENT = 20\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-tree-view-node, [ui-tree-view-node]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"tree-view-node\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"treeitem\"',\n    '[attr.aria-expanded]': 'hasChildren ? isExpanded : null',\n    '[attr.aria-selected]': 'isSelected',\n    class: 'relative block',\n  },\n  template: `\n    @if (depth > 0) {\n      <span\n        aria-hidden=\"true\"\n        class=\"border-border pointer-events-none absolute top-0 h-4 w-3 rounded-bl-md border-b border-l\"\n        [style.left]=\"connectorLeft\"\n      ></span>\n    }\n    @if (depth > 0 && !isLast) {\n      <span\n        aria-hidden=\"true\"\n        class=\"bg-border pointer-events-none absolute top-4 bottom-0 w-px\"\n        [style.left]=\"connectorLeft\"\n      ></span>\n    }\n\n    <div\n      data-tree-row\n      [attr.data-tree-id]=\"item?.id\"\n      [attr.data-tree-parent]=\"parentId\"\n      [attr.data-disabled]=\"item?.disabled ? 'true' : null\"\n      [class]=\"rowClass\"\n      [style.padding-left]=\"rowPadLeft\"\n      [tabindex]=\"item?.disabled ? -1 : 0\"\n      (click)=\"handleSelect()\"\n      (keydown)=\"handleRowKeydown($event)\"\n    >\n      @if (hasChildren) {\n        <button\n          type=\"button\"\n          class=\"focus-visible:ring-ring hover:bg-foreground/10 flex size-4 shrink-0 items-center justify-center rounded transition-transform duration-150 focus-visible:ring-2 focus-visible:outline-none\"\n          [class.rotate-90]=\"isExpanded\"\n          [attr.aria-label]=\"isExpanded ? 'Collapse' : 'Expand'\"\n          tabindex=\"-1\"\n          (click)=\"handleToggle($event)\"\n        >\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            width=\"24\"\n            height=\"24\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            stroke-width=\"2\"\n            stroke-linecap=\"round\"\n            stroke-linejoin=\"round\"\n            class=\"text-muted-foreground size-3.5\"\n            aria-hidden=\"true\"\n          >\n            <path d=\"m9 18 6-6-6-6\" />\n          </svg>\n        </button>\n      } @else {\n        <span class=\"size-4 shrink-0\"></span>\n      }\n\n      @if (tree?.showCheckboxes) {\n        <input\n          type=\"checkbox\"\n          [checked]=\"isSelected\"\n          [disabled]=\"item?.disabled\"\n          [attr.aria-label]=\"item?.label\"\n          class=\"border-input bg-background text-primary focus:ring-ring focus-visible:ring-ring size-3.5 shrink-0 rounded focus:ring-1 focus-visible:ring-2 focus-visible:outline-none\"\n          (change)=\"handleSelect()\"\n          (click)=\"$event.stopPropagation()\"\n        />\n      }\n\n      @if (tree?.showIcons) {\n        @switch (iconType) {\n          @case ('folder') {\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"24\"\n              height=\"24\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              class=\"text-primary size-4 shrink-0\"\n              aria-hidden=\"true\"\n            >\n              <path\n                d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"\n              />\n            </svg>\n          }\n          @case ('folder-open') {\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"24\"\n              height=\"24\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              class=\"text-primary size-4 shrink-0\"\n              aria-hidden=\"true\"\n            >\n              <path\n                d=\"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2\"\n              />\n            </svg>\n          }\n          @case ('file-code') {\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"24\"\n              height=\"24\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              class=\"text-muted-foreground size-4 shrink-0\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"M10 12.5 8 15l2 2.5\" />\n              <path d=\"m14 12.5 2 2.5-2 2.5\" />\n              <path d=\"M14 2v4a2 2 0 0 0 2 2h4\" />\n              <path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z\" />\n            </svg>\n          }\n          @case ('file-text') {\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"24\"\n              height=\"24\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              class=\"text-muted-foreground size-4 shrink-0\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\" />\n              <path d=\"M14 2v4a2 2 0 0 0 2 2h4\" />\n              <path d=\"M10 9H8\" />\n              <path d=\"M16 13H8\" />\n              <path d=\"M16 17H8\" />\n            </svg>\n          }\n          @case ('hash') {\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"24\"\n              height=\"24\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              class=\"text-muted-foreground size-4 shrink-0\"\n              aria-hidden=\"true\"\n            >\n              <line x1=\"4\" x2=\"20\" y1=\"9\" y2=\"9\" />\n              <line x1=\"4\" x2=\"20\" y1=\"15\" y2=\"15\" />\n              <line x1=\"10\" x2=\"8\" y1=\"3\" y2=\"21\" />\n              <line x1=\"16\" x2=\"14\" y1=\"3\" y2=\"21\" />\n            </svg>\n          }\n          @case ('user') {\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"24\"\n              height=\"24\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              class=\"text-muted-foreground size-4 shrink-0\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\" />\n              <circle cx=\"12\" cy=\"7\" r=\"4\" />\n            </svg>\n          }\n          @case ('settings') {\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"24\"\n              height=\"24\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              class=\"text-muted-foreground size-4 shrink-0\"\n              aria-hidden=\"true\"\n            >\n              <path\n                d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"\n              />\n              <circle cx=\"12\" cy=\"12\" r=\"3\" />\n            </svg>\n          }\n          @default {\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"24\"\n              height=\"24\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              class=\"text-muted-foreground size-4 shrink-0\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\" />\n              <path d=\"M14 2v4a2 2 0 0 0 2 2h4\" />\n            </svg>\n          }\n        }\n      }\n\n      <span class=\"flex-1 truncate\">{{ item?.label }}</span>\n    </div>\n\n    @if (hasChildren && isExpanded) {\n      <div role=\"group\">\n        @for (child of item!.children!; track child.id; let j = $index) {\n          <ui-tree-view-node\n            [item]=\"child\"\n            [depth]=\"depth + 1\"\n            [parentId]=\"item?.id\"\n            [isLast]=\"j === (item!.children?.length ?? 0) - 1\"\n            [tree]=\"tree\"\n          />\n        }\n      </div>\n    }\n  `,\n})\nexport class UiTreeViewNodeComponent {\n  @Input() item?: TreeViewItem\n  @Input() depth = 0\n  @Input() parentId: string | null | undefined = null\n  @Input({ transform: booleanAttribute }) isLast = false\n  @Input() tree?: UiTreeViewComponent\n\n  // Backwards compat inputs\n  @Input({ transform: booleanAttribute }) expanded = false\n  @Input({ transform: booleanAttribute }) selected = false\n  @Input('class') className?: string\n\n  get hasChildren(): boolean {\n    return !!(this.item?.children && this.item.children.length)\n  }\n\n  get isExpanded(): boolean {\n    if (this.tree && this.item) return this.tree.isExpanded(this.item.id)\n    return this.expanded\n  }\n\n  get isSelected(): boolean {\n    if (this.tree && this.item) return this.tree.isSelected(this.item.id)\n    return this.selected\n  }\n\n  get rowPadLeft(): string {\n    return `${this.depth * INDENT + 4}px`\n  }\n\n  get connectorLeft(): string {\n    return `${(this.depth - 1) * INDENT + 10}px`\n  }\n\n  get iconType(): string {\n    if (!this.item) return 'file'\n    const ic = typeof this.item.icon === 'string' ? this.item.icon.toLowerCase() : ''\n    if (ic.includes('code')) return 'file-code'\n    if (ic.includes('text')) return 'file-text'\n    if (ic.includes('hash')) return 'hash'\n    if (ic.includes('user')) return 'user'\n    if (ic.includes('setting')) return 'settings'\n    if (ic.includes('folder')) {\n      return this.isExpanded ? 'folder-open' : 'folder'\n    }\n    if (this.hasChildren) {\n      return this.isExpanded ? 'folder-open' : 'folder'\n    }\n    return 'file'\n  }\n\n  get rowClass(): string {\n    return cn(\n      'group relative flex h-8 cursor-pointer items-center gap-1.5 rounded-md pr-2 text-sm transition-colors',\n      'hover:bg-accent hover:text-accent-foreground',\n      'focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:outline-none',\n      this.item?.disabled && 'cursor-not-allowed opacity-50',\n      this.isSelected && 'bg-accent text-accent-foreground font-medium',\n      this.className,\n    )\n  }\n\n  get hostClass(): string {\n    return cn(\n      'flex items-center gap-1.5 rounded-md px-2 py-1.5',\n      this.selected && 'bg-accent text-accent-foreground',\n      this.className,\n    )\n  }\n\n  indentStyle(indent = 20): Record<string, string> {\n    return { 'padding-left': `${this.depth * indent + 8}px` }\n  }\n\n  handleToggle(e?: MouseEvent): void {\n    e?.stopPropagation()\n    if (this.tree && this.item) {\n      this.tree.toggleItem(this.item)\n    }\n  }\n\n  handleSelect(): void {\n    if (this.item?.disabled) return\n    if (this.tree && this.item) {\n      this.tree.selectItem(this.item)\n    }\n  }\n\n  handleRowKeydown(e: KeyboardEvent): void {\n    if (this.item?.disabled) return\n    const target = e.currentTarget as HTMLElement\n\n    if (e.key === 'Enter' || e.key === ' ') {\n      e.preventDefault()\n      this.handleSelect()\n      return\n    }\n\n    if (e.key === 'ArrowRight') {\n      e.preventDefault()\n      if (this.hasChildren && !this.isExpanded) {\n        this.handleToggle()\n      } else if (this.hasChildren && this.isExpanded) {\n        const tree = target.closest('[role=\"tree\"]')\n        const rows = tree\n          ? Array.from(tree.querySelectorAll<HTMLElement>('[data-tree-row]:not([data-disabled=\"true\"])'))\n          : []\n        const idx = rows.indexOf(target)\n        if (idx >= 0 && idx < rows.length - 1) rows[idx + 1]?.focus()\n      }\n      return\n    }\n\n    if (e.key === 'ArrowLeft') {\n      e.preventDefault()\n      if (this.hasChildren && this.isExpanded) {\n        this.handleToggle()\n      } else if (this.parentId) {\n        const tree = target.closest('[role=\"tree\"]')\n        const parent = tree?.querySelector<HTMLElement>(`[data-tree-row][data-tree-id=\"${CSS.escape(this.parentId)}\"]`)\n        parent?.focus()\n      }\n      return\n    }\n\n    if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n      e.preventDefault()\n      const tree = target.closest('[role=\"tree\"]')\n      const rows = tree\n        ? Array.from(tree.querySelectorAll<HTMLElement>('[data-tree-row]:not([data-disabled=\"true\"])'))\n        : []\n      const idx = rows.indexOf(target)\n      if (idx < 0) return\n      const next = e.key === 'ArrowDown' ? rows[idx + 1] : rows[idx - 1]\n      next?.focus()\n      return\n    }\n\n    if (e.key === 'Home') {\n      e.preventDefault()\n      const tree = target.closest('[role=\"tree\"]')\n      const rows = tree\n        ? Array.from(tree.querySelectorAll<HTMLElement>('[data-tree-row]:not([data-disabled=\"true\"])'))\n        : []\n      rows[0]?.focus()\n      return\n    }\n\n    if (e.key === 'End') {\n      e.preventDefault()\n      const tree = target.closest('[role=\"tree\"]')\n      const rows = tree\n        ? Array.from(tree.querySelectorAll<HTMLElement>('[data-tree-row]:not([data-disabled=\"true\"])'))\n        : []\n      rows[rows.length - 1]?.focus()\n    }\n  }\n}\n\n/**\n * Angular port of UIPKGE TreeView. Expandable hierarchical tree\n * with continuous Discord-style elbow connectors, optional checkboxes,\n * icons, keyboard navigation, and 1:1 React parity.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-tree-view, [ui-tree-view]',\n  standalone: true,\n  imports: [UiTreeViewNodeComponent],\n  host: {\n    '[attr.data-slot]': '\"tree-view\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"tree\"',\n    '[class]': 'hostClass',\n  },\n  template: `\n    @for (item of items; track item.id; let i = $index) {\n      <ui-tree-view-node [item]=\"item\" [depth]=\"0\" [isLast]=\"i === items.length - 1\" [tree]=\"this\" />\n    }\n    <ng-content />\n  `,\n})\nexport class UiTreeViewComponent implements OnInit, OnChanges {\n  @Input() items: TreeViewItem[] = []\n  @Input({ transform: booleanAttribute }) showIcons = true\n  @Input({ transform: booleanAttribute }) showCheckboxes = false\n  @Input({ transform: booleanAttribute }) defaultExpanded = false\n  @Input() selectedId: string | null = null\n  @Input('class') className?: string\n\n  @Output() select = new EventEmitter<TreeViewItem>()\n  @Output() toggle = new EventEmitter<TreeViewItem>()\n  @Output() selectedIdChange = new EventEmitter<string | null>()\n\n  announcer?: Announcer\n\n  expandedIds = new Set<string>()\n  checkedIds = new Set<string>()\n  private initialized = false\n\n  get hostClass(): string {\n    return cn('text-sm block', this.className)\n  }\n\n  ngOnInit(): void {\n    this.ensureInit()\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (changes['items'] || changes['defaultExpanded']) {\n      this.initialized = false\n      this.ensureInit()\n    }\n  }\n\n  private ensureInit(): void {\n    if (this.initialized) return\n    this.initialized = true\n    if (this.defaultExpanded) {\n      const ids = new Set<string>()\n      const walk = (list: TreeViewItem[]) => {\n        for (const i of list) {\n          if (i.children?.length) {\n            ids.add(i.id)\n            walk(i.children)\n          }\n        }\n      }\n      walk(this.items)\n      this.expandedIds = ids\n    }\n  }\n\n  isExpanded(id: string): boolean {\n    this.ensureInit()\n    return this.expandedIds.has(id)\n  }\n\n  isSelected(id: string): boolean {\n    return this.selectedId === id\n  }\n\n  toggleItem(item: TreeViewItem): void {\n    if (item.disabled) return\n    this.ensureInit()\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.toggle.emit(item)\n  }\n\n  selectItem(item: TreeViewItem): void {\n    if (item.disabled) return\n    this.selectedId = item.id\n    this.selectedIdChange.emit(item.id)\n    this.select.emit(item)\n    try {\n      this.announcer?.announce(item.label)\n    } catch {\n      /* announcer optional */\n    }\n  }\n\n  toggleCheck(item: TreeViewItem): void {\n    if (item.disabled) return\n    const next = new Set(this.checkedIds)\n    if (next.has(item.id)) next.delete(item.id)\n    else next.add(item.id)\n    this.checkedIds = next\n  }\n\n  onKeydown(event: KeyboardEvent, item: TreeViewItem, hasChildren: boolean): void {\n    if (event.key === 'Enter' || event.key === ' ') {\n      event.preventDefault()\n      this.selectItem(item)\n    } else if (event.key === 'ArrowRight' && hasChildren && !this.isExpanded(item.id)) {\n      event.preventDefault()\n      this.toggleItem(item)\n    } else if (event.key === 'ArrowLeft' && this.isExpanded(item.id)) {\n      event.preventDefault()\n      this.toggleItem(item)\n    }\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/tree-view/tree-view.component.ts"
    },
    {
      "path": "packages/registry-angular/components/tree-view/index.ts",
      "content": "export { UiTreeViewComponent, UiTreeViewNodeComponent, type TreeViewItem } from './tree-view.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/tree-view/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Indented tree of expandable nodes — file browsers, taxonomy editors, nested settings. Elbow connectors, checkbox support, and full keyboard navigation.",
  "categories": [
    "data-display"
  ]
}