{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "json-tree-view",
  "title": "Json Tree View",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/json-tree-view/json-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'\nimport { cn } from '@/lib/utils'\nimport type { JsonValue } from './types'\n\nexport type { JsonValue } from './types'\n\nfunction pathKey(path: (string | number)[]): string {\n  return path.length ? path.map((p) => (typeof p === 'number' ? `[${p}]` : `.${p}`)).join('') : '$'\n}\n\nfunction typeOf(val: JsonValue): 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null' {\n  if (val === null) return 'null'\n  if (Array.isArray(val)) return 'array'\n  return typeof val as 'object' | 'string' | 'number' | 'boolean'\n}\n\nfunction formatValue(val: JsonValue): string {\n  if (val === null) return 'null'\n  if (typeof val === 'string') return JSON.stringify(val)\n  return String(val)\n}\n\nconst typeColor: Record<string, string> = {\n  string: 'text-emerald-600 dark:text-emerald-400',\n  number: 'text-blue-600 dark:text-blue-400',\n  boolean: 'text-amber-600 dark:text-amber-400',\n  null: 'text-muted-foreground italic',\n  object: 'text-foreground',\n  array: 'text-foreground',\n}\n\nconst keyColor = 'text-violet-600 dark:text-violet-400'\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-json-tree-node, [ui-json-tree-node]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"json-tree-node\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-dimmed]': 'dimmed ? \"\" : null',\n    '[class.opacity-30]': 'dimmed',\n    '[attr.role]': '\"treeitem\"',\n    '[attr.aria-expanded]': 'isContainer ? open : null',\n  },\n  template: `\n    @if (isContainer) {\n      <div\n        data-tree-row\n        [attr.data-tree-id]=\"key\"\n        [attr.data-tree-parent]=\"parentKey\"\n        tabindex=\"0\"\n        class=\"group hover:bg-accent/40 focus-visible:ring-ring/50 -mx-1 flex items-center gap-0.5 rounded px-1 py-0.5 transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n        [style.padding-left.px]=\"indent\"\n        (click)=\"toggle()\"\n        (keydown)=\"handleRowKeydown($event)\"\n      >\n        <button\n          type=\"button\"\n          class=\"text-muted-foreground hover:text-foreground hover:bg-accent inline-flex size-4 shrink-0 items-center justify-center rounded\"\n          [attr.aria-expanded]=\"open\"\n          [attr.aria-label]=\"open ? 'Collapse' : 'Expand'\"\n          tabindex=\"-1\"\n          (click)=\"onToggleClick($event)\"\n        >\n          @if (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=\"lucide lucide-chevron-down size-3.5\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"m6 9 6 6 6-6\" />\n            </svg>\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              class=\"lucide lucide-chevron-right size-3.5\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"m9 18 6-6-6-6\" />\n            </svg>\n          }\n        </button>\n        <span class=\"text-violet-600 select-none dark:text-violet-400\">{{ isRoot ? label : '\"' + label + '\"' }}</span>\n        <span class=\"text-muted-foreground\">:</span>\n        @if (open) {\n          <span class=\"text-muted-foreground select-none\">{{ type === 'array' ? '[' : '{' }}</span>\n        } @else {\n          <span class=\"text-muted-foreground select-none\">{{ collapsedPreview }}</span>\n        }\n        @if (open) {\n          <span class=\"text-muted-foreground ml-0.5 text-xs\">{{ count }} {{ count === 1 ? 'item' : 'items' }}</span>\n        }\n        <button\n          type=\"button\"\n          class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring ml-auto inline-flex size-5 items-center justify-center rounded opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-1\"\n          title=\"Copy value\"\n          aria-label=\"Copy value\"\n          tabindex=\"-1\"\n          (click)=\"onCopyClick($event)\"\n        >\n          @if (copiedPath === key) {\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-check size-3 text-emerald-500\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"M20 6 9 17l-5-5\" />\n            </svg>\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              class=\"lucide lucide-copy size-3\"\n              aria-hidden=\"true\"\n            >\n              <rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\" />\n              <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />\n            </svg>\n          }\n        </button>\n      </div>\n\n      @if (open) {\n        <div role=\"group\">\n          @for (entry of entries; track entry.key) {\n            <ui-json-tree-node\n              [data]=\"entry.value\"\n              [path]=\"appendPath(entry.key)\"\n              [label]=\"entry.key\"\n              [isRoot]=\"false\"\n              [search]=\"search\"\n              [maxDepth]=\"maxDepth\"\n              [copiedPath]=\"copiedPath\"\n              [matchesSearchFn]=\"matchesSearchFn\"\n              [isExpandedFn]=\"isExpandedFn\"\n              [toggleFn]=\"toggleFn\"\n              (copy)=\"copy.emit($event)\"\n            />\n          }\n          <div class=\"text-muted-foreground py-0.5 select-none\" [style.padding-left.px]=\"indent\">\n            {{ type === 'array' ? ']' : '}' }}\n          </div>\n        </div>\n      }\n    } @else {\n      <div\n        data-tree-row\n        [attr.data-tree-id]=\"key\"\n        [attr.data-tree-parent]=\"parentKey\"\n        tabindex=\"0\"\n        class=\"group hover:bg-accent/40 focus-visible:ring-ring/50 -mx-1 flex items-center gap-0.5 rounded px-1 py-0.5 transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n        [style.padding-left.px]=\"indent\"\n        (click)=\"handleCopy()\"\n        (keydown)=\"handleRowKeydown($event)\"\n      >\n        <span class=\"inline-flex size-4 shrink-0\"></span>\n        @if (isRoot) {\n          <span class=\"text-muted-foreground select-none\">{{ label }}</span>\n        } @else {\n          <span class=\"text-violet-600 select-none dark:text-violet-400\">\"{{ label }}\"</span>\n        }\n        <span class=\"text-muted-foreground\">:</span>\n        <span [class]=\"valueClass\">{{ formattedValue }}</span>\n        <button\n          type=\"button\"\n          class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring ml-auto inline-flex size-5 items-center justify-center rounded opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-1\"\n          title=\"Copy value\"\n          aria-label=\"Copy value\"\n          tabindex=\"-1\"\n          (click)=\"onCopyClick($event)\"\n        >\n          @if (copiedPath === key) {\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-check size-3 text-emerald-500\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"M20 6 9 17l-5-5\" />\n            </svg>\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              class=\"lucide lucide-copy size-3\"\n              aria-hidden=\"true\"\n            >\n              <rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\" />\n              <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />\n            </svg>\n          }\n        </button>\n      </div>\n    }\n  `,\n})\nexport class UiJsonTreeNodeComponent {\n  @Input() data: JsonValue = null\n  @Input() path: (string | number)[] = []\n  @Input() label: string | number = ''\n  @Input({ transform: booleanAttribute }) isRoot = false\n  @Input() search = ''\n  @Input() maxDepth = 100\n  @Input() copiedPath: string | null = null\n\n  @Input() matchesSearchFn?: (val: JsonValue) => boolean\n  @Input() isExpandedFn?: (path: (string | number)[]) => boolean\n  @Input() toggleFn?: (path: (string | number)[]) => void\n\n  @Output() copy = new EventEmitter<{ value: string; path: string }>()\n\n  // Backwards-compat getters & setters for unit tests\n  get value(): JsonValue {\n    return this.data\n  }\n  set value(v: JsonValue) {\n    this.data = v\n  }\n\n  get key(): string {\n    return pathKey(this.path)\n  }\n\n  get parentKey(): string | null {\n    return this.path.length ? pathKey(this.path.slice(0, -1)) : null\n  }\n\n  get type(): 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null' {\n    return typeOf(this.data)\n  }\n\n  get isContainer(): boolean {\n    return this.type === 'object' || this.type === 'array'\n  }\n\n  get isExpandable(): boolean {\n    return this.isContainer\n  }\n\n  get open(): boolean {\n    return this.isExpandedFn ? this.isExpandedFn(this.path) : false\n  }\n\n  get dimmed(): boolean {\n    return !!this.search && !!this.matchesSearchFn && !this.matchesSearchFn(this.data)\n  }\n\n  get indent(): number {\n    return this.isRoot ? 0 : 20\n  }\n\n  get entries(): { key: string; value: JsonValue }[] {\n    if (Array.isArray(this.data)) return this.data.map((v, i) => ({ key: String(i), value: v }))\n    if (this.data !== null && typeof this.data === 'object') {\n      return Object.entries(this.data).map(([key, value]) => ({ key, value: value as JsonValue }))\n    }\n    return []\n  }\n\n  get count(): number {\n    return this.entries.length\n  }\n\n  get formattedValue(): string {\n    return formatValue(this.data)\n  }\n\n  get displayValue(): string {\n    return this.formattedValue\n  }\n\n  get valueClass(): string {\n    return cn(typeColor[this.type] ?? 'text-foreground', 'rounded text-left font-mono')\n  }\n\n  get collapsedPreview(): string {\n    if (this.open || !this.isContainer) return ''\n    const items = this.entries.slice(0, 3)\n    const parts = items.map(({ key, value: v }) => {\n      const vt = typeOf(v)\n      let valStr: string\n      if (vt === 'string') valStr = `\"${String(v).slice(0, 20)}\"`\n      else if (vt === 'array') valStr = '[…]'\n      else if (vt === 'object') valStr = '{…}'\n      else valStr = formatValue(v)\n      return `${Array.isArray(this.data) ? '' : `\"${key}\": `}${valStr}`\n    })\n    const suffix = this.count > 3 ? ', …' : ''\n    const open2 = this.type === 'array' ? '[' : '{'\n    const close = this.type === 'array' ? ']' : '}'\n    return `${open2}${parts.join(', ')}${suffix}${close}`\n  }\n\n  appendPath(segment: string | number): (string | number)[] {\n    return [...this.path, segment]\n  }\n\n  formatEntryKey(segment: string | number): string {\n    return String(segment)\n  }\n\n  toggle(): void {\n    this.toggleFn?.(this.path)\n  }\n\n  onToggleClick(e: MouseEvent): void {\n    e.stopPropagation()\n    this.toggle()\n  }\n\n  handleCopy(): void {\n    const str = typeof this.data === 'string' ? this.data : JSON.stringify(this.data)\n    const p = this.path.length ? this.key : String(this.label) || '$'\n    this.copy.emit({ value: str, path: p })\n  }\n\n  copyValue(): void {\n    this.handleCopy()\n  }\n\n  onCopyClick(e: MouseEvent): void {\n    e.stopPropagation()\n    this.handleCopy()\n  }\n\n  handleRowKeydown(e: KeyboardEvent): void {\n    const target = e.currentTarget as HTMLElement\n    if (e.key === 'Enter' || e.key === ' ') {\n      e.preventDefault()\n      if (this.isContainer) this.toggle()\n      else this.handleCopy()\n      return\n    }\n\n    const tree = target.closest('[role=\"tree\"]')\n    const rows = tree ? Array.from(tree.querySelectorAll<HTMLElement>('[data-tree-row]')) : []\n    const idx = rows.indexOf(target)\n\n    if (e.key === 'ArrowRight') {\n      e.preventDefault()\n      if (this.isContainer && !this.open) {\n        this.toggle()\n      } else if (this.isContainer && this.open && idx >= 0 && idx < rows.length - 1) {\n        rows[idx + 1]?.focus()\n      }\n      return\n    }\n\n    if (e.key === 'ArrowLeft') {\n      e.preventDefault()\n      if (this.isContainer && this.open) {\n        this.toggle()\n      } else if (this.parentKey && tree) {\n        const parent = tree.querySelector<HTMLElement>(`[data-tree-row][data-tree-id=\"${CSS.escape(this.parentKey)}\"]`)\n        parent?.focus()\n      }\n      return\n    }\n\n    if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n      e.preventDefault()\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      rows[0]?.focus()\n      return\n    }\n\n    if (e.key === 'End') {\n      e.preventDefault()\n      rows[rows.length - 1]?.focus()\n    }\n  }\n}\n\n/**\n * Angular port of UIPKGE JsonTreeView. Collapsible JSON viewer with\n * color-coded types, copy, search, expand-all. 1:1 React parity.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-json-tree-view, [ui-json-tree-view]',\n  standalone: true,\n  imports: [UiJsonTreeNodeComponent],\n  host: {\n    '[attr.data-slot]': '\"json-tree-view\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n  },\n  template: `\n    @if (showToolbar || showSearch) {\n      <div data-slot=\"json-tree-toolbar\" class=\"border-border flex items-center gap-2 border-b px-3 py-2\">\n        <div class=\"flex items-center gap-1.5\">\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-braces text-muted-foreground size-4\"\n            aria-hidden=\"true\"\n          >\n            <path d=\"M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5a2 2 0 0 0 2 2h1\" />\n            <path d=\"M16 21h1a2 2 0 0 0 2-2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1\" />\n          </svg>\n          <span class=\"text-muted-foreground text-xs\">{{ summary }}</span>\n        </div>\n        <div class=\"ml-auto flex items-center gap-1\">\n          @if (showSearch) {\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 size-3.5 -translate-y-1/2\"\n                aria-hidden=\"true\"\n              >\n                <circle cx=\"11\" cy=\"11\" r=\"8\" />\n                <path d=\"m21 21-4.3-4.3\" />\n              </svg>\n              <input\n                data-slot=\"json-tree-search\"\n                type=\"text\"\n                [value]=\"search\"\n                (input)=\"onSearchInput($event)\"\n                placeholder=\"Filter...\"\n                aria-label=\"Filter JSON tree\"\n                class=\"border-input bg-muted/40 focus:border-ring focus:ring-ring/30 h-7 w-32 rounded-md pr-2 pl-7 text-xs transition-[width] outline-none focus:w-44 focus:ring-2\"\n              />\n            </div>\n          }\n          @if (search) {\n            <span class=\"text-muted-foreground text-xs\">\n              {{ searchMatchCount }} match{{ searchMatchCount === 1 ? '' : 'es' }}\n            </span>\n          }\n          <button\n            type=\"button\"\n            class=\"text-muted-foreground hover:text-foreground hover:bg-accent inline-flex size-7 items-center justify-center rounded-md transition-colors\"\n            title=\"Expand all\"\n            aria-label=\"Expand all\"\n            (click)=\"expandAll()\"\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-unfold-vertical size-4\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"M12 22v-6\" />\n              <path d=\"M12 8V2\" />\n              <path d=\"M4 12H2\" />\n              <path d=\"M10 12H8\" />\n              <path d=\"M16 12h-2\" />\n              <path d=\"M22 12h-2\" />\n              <path d=\"m15 19-3 3-3-3\" />\n              <path d=\"m9 5 3-3 3 3\" />\n            </svg>\n          </button>\n          <button\n            type=\"button\"\n            class=\"text-muted-foreground hover:text-foreground hover:bg-accent inline-flex size-7 items-center justify-center rounded-md transition-colors\"\n            title=\"Collapse all\"\n            aria-label=\"Collapse all\"\n            (click)=\"collapseAll()\"\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-fold-vertical size-4\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"M12 22v-6\" />\n              <path d=\"M12 8V2\" />\n              <path d=\"M4 12H2\" />\n              <path d=\"M10 12H8\" />\n              <path d=\"M16 12h-2\" />\n              <path d=\"M22 12h-2\" />\n              <path d=\"m15 5-3-3-3 3\" />\n              <path d=\"m9 19 3 3 3-3\" />\n            </svg>\n          </button>\n        </div>\n      </div>\n    }\n\n    <div\n      class=\"min-h-0 flex-1 overflow-auto p-2\"\n      role=\"tree\"\n      [attr.aria-label]=\"rootLabel\"\n      [style.max-height]=\"maxHeight\"\n    >\n      <ui-json-tree-node\n        [data]=\"data\"\n        [path]=\"[]\"\n        [label]=\"rootLabel\"\n        [isRoot]=\"true\"\n        [search]=\"search\"\n        [maxDepth]=\"maxDepth\"\n        [copiedPath]=\"copiedPath\"\n        [matchesSearchFn]=\"matchesSearchBound\"\n        [isExpandedFn]=\"isExpandedBound\"\n        [toggleFn]=\"toggleBound\"\n        (copy)=\"onCopyNode($event)\"\n      />\n    </div>\n  `,\n})\nexport class UiJsonTreeViewComponent implements OnInit, OnChanges {\n  @Input() data: JsonValue = null\n  @Input() expandDepth = 1\n  @Input() maxDepth = 100\n  @Input({ transform: booleanAttribute }) showSearch = true\n  @Input({ transform: booleanAttribute }) showToolbar = true\n  @Input() rootLabel = 'root'\n  @Input() maxHeight?: string\n  @Input('class') className?: string\n\n  @Output() copy = new EventEmitter<{ value: string; path: string }>()\n\n  search = ''\n  copiedPath: string | null = null\n  expanded = new Set<string>()\n  forcedExpanded: boolean | null = null\n\n  readonly matchesSearchBound = (val: JsonValue): boolean => this.matchesSearch(val)\n  readonly isExpandedBound = (path: (string | number)[]): boolean => this.expanded.has(pathKey(path))\n  readonly toggleBound = (path: (string | number)[]): void => this.toggle(path)\n\n  get hostClass(): string {\n    return cn('bg-background flex flex-col overflow-hidden rounded-lg border font-mono text-sm', this.className)\n  }\n\n  get summary(): string {\n    const t = typeOf(this.data)\n    if (t === 'array') return `Array(${(this.data as JsonValue[]).length})`\n    if (t === 'object') return `Object(${Object.keys(this.data as object).length})`\n    return t\n  }\n\n  get searchMatchCount(): number {\n    if (!this.search) return 0\n    let count = 0\n    const term = this.search.toLowerCase()\n    const walk = (v: JsonValue) => {\n      if (v === null) {\n        if ('null'.includes(term)) count++\n        return\n      }\n      if (typeof v === 'string') {\n        if (v.toLowerCase().includes(term)) count++\n        return\n      }\n      if (typeof v === 'number' || typeof v === 'boolean') {\n        if (String(v).includes(term)) count++\n        return\n      }\n      if (Array.isArray(v)) {\n        v.forEach(walk)\n        return\n      }\n      if (typeof v === 'object') {\n        Object.entries(v).forEach(([k, val]) => {\n          if (k.toLowerCase().includes(term)) count++\n          walk(val)\n        })\n      }\n    }\n    walk(this.data)\n    return count\n  }\n\n  ngOnInit(): void {\n    this.resetExpanded()\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (changes['data'] || changes['expandDepth']) {\n      this.resetExpanded()\n    }\n  }\n\n  private resetExpanded(): void {\n    const next = new Set<string>()\n    const walk = (val: JsonValue, path: (string | number)[] = [], depth = 0) => {\n      if (depth >= this.expandDepth) return\n      if (val !== null && typeof val === 'object') {\n        next.add(pathKey(path))\n        const entries = Array.isArray(val) ? val.map((v, i) => [i, v] as const) : Object.entries(val)\n        for (const [k, v] of entries) {\n          walk(v as JsonValue, [...path, k], depth + 1)\n        }\n      }\n    }\n    walk(this.data)\n    this.expanded = next\n  }\n\n  toggle(path: (string | number)[]): void {\n    const k = pathKey(path)\n    const next = new Set(this.expanded)\n    if (next.has(k)) next.delete(k)\n    else next.add(k)\n    this.expanded = next\n  }\n\n  expandAll(): void {\n    this.forcedExpanded = true\n    const next = new Set<string>()\n    const walk = (val: JsonValue, path: (string | number)[] = [], depth = 0) => {\n      if (depth >= this.maxDepth) return\n      if (val !== null && typeof val === 'object') {\n        next.add(pathKey(path))\n        const entries = Array.isArray(val) ? val.map((v, i) => [i, v] as const) : Object.entries(val)\n        for (const [k, v] of entries) {\n          walk(v as JsonValue, [...path, k], depth + 1)\n        }\n      }\n    }\n    walk(this.data)\n    this.expanded = next\n  }\n\n  collapseAll(): void {\n    this.forcedExpanded = false\n    this.expanded = new Set()\n  }\n\n  onSearchInput(event: Event): void {\n    this.search = (event.target as HTMLInputElement).value\n    if (!this.search) {\n      this.resetExpanded()\n      return\n    }\n    const next = new Set<string>()\n    const walk = (val: JsonValue, path: (string | number)[] = [], depth = 0) => {\n      if (depth >= this.maxDepth) return\n      if (val !== null && typeof val === 'object') {\n        if (this.matchesSearch(val)) next.add(pathKey(path))\n        const entries = Array.isArray(val) ? val.map((v, i) => [i, v] as const) : Object.entries(val)\n        for (const [k, v] of entries) {\n          walk(v as JsonValue, [...path, k], depth + 1)\n        }\n      }\n    }\n    walk(this.data)\n    this.expanded = next\n  }\n\n  matchesSearch(val: JsonValue): boolean {\n    if (!this.search) return true\n    const term = this.search.toLowerCase()\n    const walk = (v: JsonValue): boolean => {\n      if (v === null) return 'null'.includes(term)\n      if (typeof v === 'string') return v.toLowerCase().includes(term)\n      if (typeof v === 'number' || typeof v === 'boolean') return String(v).includes(term)\n      if (Array.isArray(v)) return v.some(walk)\n      if (typeof v === 'object') {\n        return Object.entries(v).some(([k, value]) => k.toLowerCase().includes(term) || walk(value))\n      }\n      return false\n    }\n    return walk(val)\n  }\n\n  async onCopyNode(event: { value: string; path: string }): Promise<void> {\n    try {\n      await navigator.clipboard.writeText(event.value)\n      this.copiedPath = event.path\n      this.copy.emit(event)\n      setTimeout(() => {\n        if (this.copiedPath === event.path) this.copiedPath = null\n      }, 1200)\n    } catch {\n      this.copy.emit(event)\n    }\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/json-tree-view/json-tree-view.component.ts"
    },
    {
      "path": "packages/registry-angular/components/json-tree-view/types.ts",
      "content": "export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/json-tree-view/types.ts"
    },
    {
      "path": "packages/registry-angular/components/json-tree-view/index.ts",
      "content": "export { UiJsonTreeViewComponent, UiJsonTreeNodeComponent } from './json-tree-view.component'\nexport type { JsonValue } from './types'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/json-tree-view/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Collapsible JSON tree viewer with color-coded value types, click-to-copy, live search/filter, and expand/collapse-all controls. Renders objects, arrays, and primitives with configurable depth and contained scrolling.",
  "categories": [
    "display",
    "data"
  ]
}