{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "tree-select",
  "title": "Tree Select",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/tree-select/tree-select.component.ts",
      "content": "import {\n  AfterViewInit,\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  Output,\n  SimpleChanges,\n  ViewChild,\n  booleanAttribute,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport {\n  UiPopoverComponent,\n  UiPopoverContentComponent,\n  UiPopoverTriggerComponent,\n} from '@/ui/popover/popover.component'\nimport { treeSelectTriggerVariants } from './tree-select.variants'\nimport { UiTreeSelectNodeComponent } from './tree-select-node.component'\nimport type { TreeSelectNode as TreeNode } from './types'\n\nexport type TreeSelectValue = string | string[] | null\nexport type TreeSelectSize = 'sm' | 'default' | 'lg'\n\nfunction collectAllExpandable(nodes: TreeNode[]): string[] {\n  const ids: string[] = []\n  const walk = (list: TreeNode[]) => {\n    for (const n of list) {\n      if (n.children?.length) {\n        ids.push(n.value)\n        walk(n.children)\n      }\n    }\n  }\n  walk(nodes)\n  return ids\n}\n\nfunction findNode(nodes: TreeNode[], value: string): TreeNode | undefined {\n  for (const n of nodes) {\n    if (n.value === value) return n\n    if (n.children) {\n      const found = findNode(n.children, value)\n      if (found) return found\n    }\n  }\n  return undefined\n}\n\nfunction findLabels(nodes: TreeNode[], values: string[]): string[] {\n  return values.map((v) => findNode(nodes, v)?.label ?? v)\n}\n\nfunction collectLeafValues(node: TreeNode): string[] {\n  if (!node.children?.length) return [node.value]\n  const vals: string[] = []\n  for (const c of node.children) vals.push(...collectLeafValues(c))\n  return vals\n}\n\n/**\n * Angular port of UIPKGE TreeSelect, 1:1 with the React component: a Popover whose trigger\n * is a combobox button (label, or up to three labels + \"+N\" in multiple mode) and whose\n * content is a searchable role=tree of TreeSelectNode rows. Single mode commits and closes;\n * multiple mode shows checkboxes, a parent toggles all its leaf descendants, and a footer\n * shows \"N selected / Clear all\". Searching keeps matches plus their ancestors and expands\n * those ancestors. `value` / `defaultValue` / `valueChange` work controlled or uncontrolled.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-tree-select, [ui-tree-select]',\n  standalone: true,\n  imports: [UiPopoverComponent, UiPopoverTriggerComponent, UiPopoverContentComponent, UiTreeSelectNodeComponent],\n  // Block (not `contents`) so parent spacing utilities reach it like React's trigger button.\n  host: { '[class]': '\"block\"' },\n  template: `\n    <ui-popover [open]=\"isOpen()\" (openChange)=\"setOpen($event)\">\n      <button\n        #triggerEl\n        ui-popover-trigger\n        type=\"button\"\n        role=\"combobox\"\n        [disabled]=\"disabled || loading\"\n        data-uipkge=\"\"\n        data-slot=\"tree-select\"\n        [class]=\"triggerClasses\"\n      >\n        <span [class]=\"labelClass\">{{ displayLabel }}</span>\n        <span class=\"flex shrink-0 items-center gap-1\">\n          @if (loading) {\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=\"lucide lucide-loader-circle text-muted-foreground size-4 animate-spin\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n            </svg>\n          } @else if (clearable && hasValue && !disabled) {\n            <span\n              role=\"button\"\n              tabindex=\"0\"\n              aria-label=\"Clear\"\n              class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 flex size-4 items-center justify-center rounded transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n              (click)=\"clearAll($event)\"\n              (keydown)=\"onClearKeydown($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=\"lucide lucide-x size-4\"\n                aria-hidden=\"true\"\n              >\n                <path d=\"M18 6 6 18\" />\n                <path d=\"m6 6 12 12\" />\n              </svg>\n            </span>\n          } @else {\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              [attr.class]=\"chevronClass\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"m6 9 6 6 6-6\" />\n            </svg>\n          }\n        </span>\n      </button>\n\n      <ui-popover-content class=\"p-0\" align=\"start\" [sideOffset]=\"4\" (openAutoFocus)=\"onPanelOpen()\">\n        <div #body class=\"flex max-h-80 flex-col\">\n          @if (searchable) {\n            <div class=\"border-b p-2\">\n              <div class=\"relative\">\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=\"lucide lucide-search text-muted-foreground absolute top-1/2 left-2.5 size-4 -translate-y-1/2\"\n                  aria-hidden=\"true\"\n                >\n                  <path d=\"m21 21-4.34-4.34\" />\n                  <circle cx=\"11\" cy=\"11\" r=\"8\" />\n                </svg>\n                <input\n                  [value]=\"search()\"\n                  (input)=\"onSearch($any($event.target).value)\"\n                  [placeholder]=\"searchPlaceholder\"\n                  aria-label=\"Search tree\"\n                  class=\"border-input focus-visible:ring-ring/50 h-9 w-full rounded-md border bg-transparent pl-8 text-sm shadow-xs outline-none focus-visible:ring-[3px]\"\n                />\n              </div>\n            </div>\n          }\n\n          @if (loading) {\n            <div class=\"text-muted-foreground flex items-center justify-center gap-2 py-6 text-sm\">\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=\"lucide lucide-loader-circle size-4 animate-spin\"\n                aria-hidden=\"true\"\n              >\n                <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n              </svg>\n              Loading...\n            </div>\n          } @else if (data.length === 0 || (filteredIds() && filteredIds()!.size === 0)) {\n            <div class=\"text-muted-foreground py-6 text-center text-sm\">{{ emptyText }}</div>\n          } @else {\n            <div class=\"flex-1 overflow-y-auto p-1\" role=\"tree\">\n              @for (node of data; track node.value) {\n                @if (!filteredIds() || filteredIds()!.has(node.value)) {\n                  <div\n                    ui-tree-select-node\n                    [node]=\"node\"\n                    [depth]=\"0\"\n                    [multiple]=\"multiple\"\n                    [expandedIds]=\"expandedIds()\"\n                    [selectedValues]=\"selectedValues\"\n                    [filteredIds]=\"filteredIds()\"\n                    (toggle)=\"toggleNode($event)\"\n                    (select)=\"selectNode($event)\"\n                  ></div>\n                }\n              }\n            </div>\n          }\n\n          @if (multiple && selectedCount > 0) {\n            <div class=\"flex items-center justify-between border-t px-2 py-1.5 text-xs\">\n              <span class=\"text-muted-foreground\">{{ selectedCount }} selected</span>\n              <button\n                type=\"button\"\n                class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 rounded focus-visible:ring-2 focus-visible:outline-none\"\n                (click)=\"clearAll()\"\n              >\n                Clear all\n              </button>\n            </div>\n          }\n        </div>\n      </ui-popover-content>\n    </ui-popover>\n  `,\n})\nexport class UiTreeSelectComponent implements OnChanges, AfterViewInit {\n  /** Controlled value (pair with `valueChange`). Leave unset for uncontrolled use. */\n  @Input() value?: TreeSelectValue\n  @Input() defaultValue?: TreeSelectValue\n  @Input() data: TreeNode[] = []\n  @Input({ transform: booleanAttribute }) multiple = false\n  @Input() placeholder = 'Select...'\n  @Input({ transform: booleanAttribute }) searchable = true\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input({ transform: booleanAttribute }) loading = false\n  @Input({ transform: booleanAttribute }) clearable = true\n  @Input({ transform: booleanAttribute }) defaultExpandAll = false\n  @Input() size: TreeSelectSize = 'default'\n  @Input() emptyText = 'No results found.'\n  @Input() searchPlaceholder = 'Search...'\n  @Input('class') className?: string\n\n  /** React `onValueChange`. */\n  @Output() valueChange = new EventEmitter<TreeSelectValue>()\n  /** React `onChange`. */\n  @Output() change = new EventEmitter<TreeSelectValue>()\n  /** React `onSelect(node)`. */\n  @Output() select = new EventEmitter<TreeNode>()\n  /** React `onClear`. */\n  @Output() clear = new EventEmitter<void>()\n\n  @ViewChild('triggerEl', { static: true }) private triggerEl?: ElementRef<HTMLButtonElement>\n  @ViewChild('body', { static: true }) private body?: ElementRef<HTMLElement>\n\n  readonly isOpen = signal(false)\n  readonly search = signal('')\n  readonly expandedIds = signal<Set<string>>(new Set())\n  private readonly internalValue = signal<TreeSelectValue | undefined>(undefined)\n\n  ngOnChanges(changes: SimpleChanges): void {\n    // Re-collect expandable nodes when defaultExpandAll toggles or data changes.\n    if ((changes['defaultExpandAll'] || changes['data']) && this.defaultExpandAll) {\n      this.expandedIds.set(new Set(collectAllExpandable(this.data)))\n    }\n  }\n\n  ngAfterViewInit(): void {\n    // Radix Slot: the child's data-slot wins over PopoverTrigger's (Angular host bindings would win).\n    this.triggerEl?.nativeElement.setAttribute('data-slot', 'tree-select')\n  }\n\n  get currentValue(): TreeSelectValue {\n    if (this.value !== undefined) return this.value\n    const v = this.internalValue()\n    return v === undefined ? (this.defaultValue ?? null) : v\n  }\n\n  setOpen(open: boolean): void {\n    this.isOpen.set(open)\n    // Clear search when the popover closes.\n    if (!open) this.search.set('')\n  }\n\n  /** Content width tracks the trigger (React passes `style` to PopoverContent). */\n  onPanelOpen(): void {\n    const panel = this.body?.nativeElement.parentElement\n    if (panel) panel.style.width = 'var(--radix-popover-trigger-width)'\n  }\n\n  get selectedValues(): Set<string> {\n    const v = this.currentValue\n    if (v == null) return new Set()\n    return new Set(Array.isArray(v) ? v : [v])\n  }\n\n  get selectedCount(): number {\n    const v = this.currentValue\n    return Array.isArray(v) ? v.length : 0\n  }\n\n  get displayLabel(): string {\n    const v = this.currentValue\n    if (this.multiple) {\n      const vals = Array.isArray(v) ? v : []\n      if (vals.length === 0) return this.placeholder\n      const labels = findLabels(this.data, vals)\n      if (labels.length <= 3) return labels.join(', ')\n      return `${labels.slice(0, 3).join(', ')} +${labels.length - 3}`\n    }\n    if (v == null) return this.placeholder\n    return findNode(this.data, v as string)?.label ?? String(v)\n  }\n\n  get hasValue(): boolean {\n    const v = this.currentValue\n    if (this.multiple) return Array.isArray(v) && v.length > 0\n    return v != null\n  }\n\n  /** Search: a node is visible if it or any descendant matches. */\n  filteredIds(): Set<string> | null {\n    const q = this.search().trim().toLowerCase()\n    if (!q) return null\n    const visible = new Set<string>()\n    const walk = (nodes: TreeNode[]): boolean => {\n      let anyMatch = false\n      for (const n of nodes) {\n        const selfMatch = n.label.toLowerCase().includes(q)\n        const childMatch = n.children?.length ? walk(n.children) : false\n        if (selfMatch || childMatch) {\n          visible.add(n.value)\n          anyMatch = true\n        }\n      }\n      return anyMatch\n    }\n    walk(this.data)\n    return visible\n  }\n\n  onSearch(value: string): void {\n    this.search.set(value)\n    // Auto-expand ancestors of search matches (React does this in an effect).\n    const q = value.trim().toLowerCase()\n    if (!q) return\n    const next = new Set(this.expandedIds())\n    let changed = false\n    const walk = (nodes: TreeNode[]): boolean => {\n      let anyMatch = false\n      for (const n of nodes) {\n        const selfMatch = n.label.toLowerCase().includes(q)\n        const childMatch = n.children?.length ? walk(n.children) : false\n        if (selfMatch || childMatch) {\n          anyMatch = true\n          if (childMatch && !next.has(n.value)) {\n            next.add(n.value)\n            changed = true\n          }\n        }\n      }\n      return anyMatch\n    }\n    walk(this.data)\n    if (changed) this.expandedIds.set(next)\n  }\n\n  private commit(next: TreeSelectValue): void {\n    if (this.value === undefined) this.internalValue.set(next)\n    this.valueChange.emit(next)\n    this.change.emit(next)\n  }\n\n  toggleNode(node: TreeNode): void {\n    if (node.disabled) return\n    const next = new Set(this.expandedIds())\n    if (next.has(node.value)) next.delete(node.value)\n    else next.add(node.value)\n    this.expandedIds.set(next)\n  }\n\n  selectNode(node: TreeNode): void {\n    if (node.disabled) return\n    if (!this.multiple) {\n      this.commit(node.value)\n      this.select.emit(node)\n      this.setOpen(false)\n      return\n    }\n    // Multi-select: toggle. For parent nodes, toggle all leaf descendants.\n    const v = this.currentValue\n    const current = Array.isArray(v) ? [...v] : []\n    const leaves = collectLeafValues(node)\n    const allSelected = leaves.every((l) => current.includes(l))\n    const next = allSelected\n      ? current.filter((l) => !leaves.includes(l))\n      : [...current, ...leaves.filter((l) => !current.includes(l))]\n    this.commit(next)\n    this.select.emit(node)\n  }\n\n  clearAll(event?: Event): void {\n    event?.stopPropagation()\n    if (this.disabled) return\n    this.clear.emit()\n    this.commit(this.multiple ? [] : null)\n  }\n\n  onClearKeydown(e: KeyboardEvent): void {\n    if (e.key === 'Enter' || e.key === ' ') {\n      e.preventDefault()\n      this.clearAll(e)\n    }\n  }\n\n  get triggerClasses(): string {\n    return cn(treeSelectTriggerVariants({ size: this.size }), this.className)\n  }\n\n  get labelClass(): string {\n    return cn('flex-1 truncate text-left', this.hasValue ? 'text-foreground' : 'text-muted-foreground')\n  }\n\n  get chevronClass(): string {\n    return cn(\n      'lucide lucide-chevron-down',\n      'text-muted-foreground size-4 shrink-0 transition-transform duration-200',\n      this.isOpen() && 'rotate-180',\n    )\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/tree-select/tree-select.component.ts"
    },
    {
      "path": "packages/registry-angular/components/tree-select/tree-select-node.component.ts",
      "content": "import {\n  Component,\n  EventEmitter,\n  Input,\n  Output,\n  booleanAttribute,\n  forwardRef,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport type { TreeSelectNode as TreeSelectNodeData } from './types'\n\nfunction collectValues(node: TreeSelectNodeData): string[] {\n  const vals: string[] = []\n  const walk = (n: TreeSelectNodeData) => {\n    if (n.children?.length) for (const c of n.children) walk(c)\n    else vals.push(n.value)\n  }\n  walk(node)\n  return vals\n}\n\nfunction getTreeRows(from: HTMLElement): HTMLElement[] {\n  const tree = from.closest('[role=\"tree\"]')\n  if (!tree) return []\n  return Array.from(tree.querySelectorAll<HTMLElement>('[data-tree-row]:not([data-disabled=\"true\"])'))\n}\n\nfunction focusRow(row: HTMLElement | null | undefined): void {\n  row?.focus()\n}\n\n/**\n * One TreeSelect row (React `TreeSelectNode`): a role=treeitem wrapper holding the focusable\n * row (expand chevron, optional checkbox with indeterminate state, label) and, when expanded,\n * a role=group of child rows. Arrow keys / Home / End move focus between visible rows,\n * ArrowRight / ArrowLeft expand, collapse or jump to the parent, Enter / Space select.\n * Put it on a div: `<div ui-tree-select-node [node]=\"...\">` (React renders a div).\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-tree-select-node, [ui-tree-select-node]',\n  standalone: true,\n  // Recursive: child rows are this component.\n  imports: [forwardRef(() => UiTreeSelectNodeComponent)],\n  host: {\n    role: 'treeitem',\n    '[class]': '\"block\"',\n    '[attr.aria-expanded]': 'hasChildren ? isExpanded : null',\n    '[attr.aria-selected]': 'isSelected',\n  },\n  template: `\n    <div\n      data-tree-row=\"\"\n      [attr.data-tree-id]=\"node.value\"\n      [attr.data-tree-parent]=\"parentValue ?? null\"\n      [attr.data-disabled]=\"node.disabled ? 'true' : null\"\n      [class]=\"rowClass\"\n      [style.padding-left]=\"indent\"\n      [attr.tabindex]=\"node.disabled ? -1 : 0\"\n      (click)=\"handleSelect()\"\n      (keydown)=\"handleRowKeydown($event)\"\n    >\n      @if (hasChildren) {\n        <button\n          type=\"button\"\n          [class]=\"toggleClass\"\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=\"lucide lucide-chevron-right 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 (multiple) {\n        <input\n          type=\"checkbox\"\n          [checked]=\"isFullyChecked || isChecked\"\n          [indeterminate]=\"isChecked && !isFullyChecked\"\n          [disabled]=\"!!node.disabled\"\n          class=\"border-input text-primary focus:ring-ring size-3.5 shrink-0 rounded focus:ring-1\"\n          (change)=\"handleCheckboxChange($event)\"\n          (click)=\"$event.stopPropagation()\"\n        />\n      }\n\n      <span class=\"flex-1 truncate\">{{ node.label }}</span>\n    </div>\n\n    @if (hasChildren && isExpanded) {\n      <div role=\"group\">\n        @for (child of node.children; track child.value) {\n          @if (!filteredIds || filteredIds.has(child.value)) {\n            <div\n              ui-tree-select-node\n              [node]=\"child\"\n              [depth]=\"depth + 1\"\n              [multiple]=\"multiple\"\n              [expandedIds]=\"expandedIds\"\n              [selectedValues]=\"selectedValues\"\n              [filteredIds]=\"filteredIds\"\n              [parentValue]=\"node.value\"\n              (toggle)=\"toggle.emit($event)\"\n              (select)=\"select.emit($event)\"\n            ></div>\n          }\n        }\n      </div>\n    }\n  `,\n})\nexport class UiTreeSelectNodeComponent {\n  @Input() node: TreeSelectNodeData = { value: '', label: '' }\n  @Input() depth = 0\n  @Input({ transform: booleanAttribute }) multiple = false\n  @Input() expandedIds: Set<string> = new Set()\n  @Input() selectedValues: Set<string> = new Set()\n  @Input() filteredIds: Set<string> | null = null\n  @Input() parentValue: string | null = null\n  /** React `onToggle(node)`. */\n  @Output() toggle = new EventEmitter<TreeSelectNodeData>()\n  /** React `onSelect(node)`. */\n  @Output() select = new EventEmitter<TreeSelectNodeData>()\n\n  get hasChildren(): boolean {\n    return !!this.node.children?.length\n  }\n\n  get isExpanded(): boolean {\n    return this.expandedIds.has(this.node.value)\n  }\n\n  /** Partially checked (some but not all leaf descendants), or selected in single mode. */\n  get isChecked(): boolean {\n    if (!this.multiple) return this.selectedValues.has(this.node.value)\n    if (this.selectedValues.has(this.node.value)) return true\n    if (!this.hasChildren) return false\n    const descendants = collectValues(this.node)\n    const selected = descendants.filter((v) => this.selectedValues.has(v))\n    return selected.length > 0 && selected.length < descendants.length\n  }\n\n  get isFullyChecked(): boolean {\n    if (!this.multiple) return false\n    if (this.selectedValues.has(this.node.value)) return true\n    if (!this.hasChildren) return false\n    const descendants = collectValues(this.node)\n    return descendants.length > 0 && descendants.every((v) => this.selectedValues.has(v))\n  }\n\n  get isSelected(): boolean {\n    return !this.multiple ? this.selectedValues.has(this.node.value) : this.isFullyChecked || this.isChecked\n  }\n\n  get indent(): string {\n    return `${this.depth * 20 + 8}px`\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 focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:outline-none',\n      this.node.disabled && 'cursor-not-allowed opacity-50',\n      !this.multiple && this.selectedValues.has(this.node.value) && 'bg-accent text-accent-foreground font-medium',\n    )\n  }\n\n  get toggleClass(): string {\n    return cn(\n      'focus-visible:ring-ring flex size-4 shrink-0 items-center justify-center rounded transition-transform duration-150 focus-visible:ring-2 focus-visible:outline-none',\n      'hover:bg-foreground/10',\n      this.isExpanded && 'rotate-90',\n    )\n  }\n\n  handleToggle(e: Event): void {\n    e.stopPropagation()\n    this.toggle.emit(this.node)\n  }\n\n  handleSelect(): void {\n    if (this.node.disabled) return\n    this.select.emit(this.node)\n  }\n\n  handleCheckboxChange(e: Event): void {\n    e.stopPropagation()\n    if (this.node.disabled) return\n    this.select.emit(this.node)\n  }\n\n  handleRowKeydown(e: KeyboardEvent): void {\n    if (this.node.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.toggle.emit(this.node)\n      } else if (this.hasChildren && this.isExpanded) {\n        const rows = getTreeRows(target)\n        const idx = rows.indexOf(target)\n        if (idx >= 0 && idx < rows.length - 1) focusRow(rows[idx + 1])\n      }\n      return\n    }\n\n    if (e.key === 'ArrowLeft') {\n      e.preventDefault()\n      if (this.hasChildren && this.isExpanded) {\n        this.toggle.emit(this.node)\n      } else if (this.parentValue) {\n        const tree = target.closest('[role=\"tree\"]')\n        focusRow(tree?.querySelector<HTMLElement>(`[data-tree-row][data-tree-id=\"${CSS.escape(this.parentValue)}\"]`))\n      }\n      return\n    }\n\n    if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n      e.preventDefault()\n      const rows = getTreeRows(target)\n      const idx = rows.indexOf(target)\n      if (idx < 0) return\n      focusRow(e.key === 'ArrowDown' ? rows[idx + 1] : rows[idx - 1])\n      return\n    }\n\n    if (e.key === 'Home') {\n      e.preventDefault()\n      focusRow(getTreeRows(target)[0])\n      return\n    }\n\n    if (e.key === 'End') {\n      e.preventDefault()\n      const rows = getTreeRows(target)\n      focusRow(rows[rows.length - 1])\n    }\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/tree-select/tree-select-node.component.ts"
    },
    {
      "path": "packages/registry-angular/components/tree-select/tree-select.variants.ts",
      "content": "import { cva } from 'class-variance-authority'\n\nexport const treeSelectTriggerVariants = cva(\n  'flex w-full items-center justify-between gap-2 rounded-md border border-input bg-transparent text-sm shadow-xs transition-[color,box-shadow] outline-none hover:border-ring/50 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',\n  {\n    variants: {\n      size: {\n        sm: 'h-8 text-xs px-2.5',\n        default: 'h-9 text-sm px-3',\n        lg: 'h-11 text-base px-4',\n      },\n    },\n    defaultVariants: {\n      size: 'default',\n    },\n  },\n)\n\nexport type TreeSelectVariants = ReturnType<typeof treeSelectTriggerVariants>\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/tree-select/tree-select.variants.ts"
    },
    {
      "path": "packages/registry-angular/components/tree-select/types.ts",
      "content": "export interface TreeSelectNode {\n  value: string\n  label: string\n  disabled?: boolean\n  children?: TreeSelectNode[]\n  [key: string]: unknown\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/tree-select/types.ts"
    },
    {
      "path": "packages/registry-angular/components/tree-select/index.ts",
      "content": "export { UiTreeSelectComponent, type TreeSelectSize, type TreeSelectValue } from './tree-select.component'\n// The data type gets the canonical `TreeSelectNode` name (consumers type their trees with\n// it); the row component is exported as UiTreeSelectNodeComponent (React: TreeSelectNodeComponent).\nexport { UiTreeSelectNodeComponent } from './tree-select-node.component'\nexport { treeSelectTriggerVariants, type TreeSelectVariants } from './tree-select.variants'\nexport type { TreeSelectNode } from './types'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/tree-select/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/popover.json"
  ],
  "description": "Select from tree-structured data with expand/collapse nodes, single or multi-select with checkboxes, and search filtering. Dropdown shows a nested tree; parent selection cascades to leaf descendants.",
  "categories": [
    "control",
    "form"
  ]
}