{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "xml-tree-view",
  "title": "Xml Tree View",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/xml-tree-view/xml-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'\n\nexport interface XmlAttr {\n  name: string\n  value: string\n}\n\nexport interface XmlNode {\n  type: 'element' | 'text' | 'comment' | 'cdata'\n  name?: string\n  attributes?: Record<string, string>\n  attrs?: XmlAttr[]\n  children?: XmlNode[]\n  value?: string\n  text?: string\n}\n\nexport function pathKey(path: string[]): string {\n  return path.length ? '/' + path.join('/') : '/'\n}\n\nexport function isExpandable(node: XmlNode): boolean {\n  if (node.type !== 'element') return false\n  if (!node.children || node.children.length === 0) return false\n  if (\n    node.children.length === 1 &&\n    (node.children[0].type === 'text' || (!node.children[0].type && node.children[0].value))\n  ) {\n    return false\n  }\n  return true\n}\n\nexport function countElements(node: XmlNode): number {\n  let n = node.type === 'element' ? 1 : 0\n  for (const c of node.children ?? []) n += countElements(c)\n  return n\n}\n\nfunction escapeAttr(s: string): string {\n  return s.replace(/&/g, '&amp;').replace(/\"/g, '&quot;').replace(/</g, '&lt;')\n}\n\nfunction escapeText(s: string): string {\n  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;')\n}\n\nexport function serializeXml(node: XmlNode, indent = 0): string {\n  const pad = '  '.repeat(indent)\n  const val = node.text ?? node.value ?? ''\n\n  if (node.type === 'text') return val\n  if (node.type === 'comment') return `${pad}<!--${val}-->`\n  if (node.type === 'cdata') return `${pad}<![CDATA[${val}]]>`\n\n  const attrs = Object.entries(node.attributes ?? {})\n    .map(([k, v]) => ` ${k}=\"${escapeAttr(v)}\"`)\n    .join('')\n\n  const children = node.children ?? []\n  if (children.length === 0) {\n    return `${pad}<${node.name}${attrs} />`\n  }\n\n  if (children.length === 1 && (children[0].type === 'text' || (!children[0].type && children[0].value))) {\n    const text = children[0].text ?? children[0].value ?? ''\n    return `${pad}<${node.name}${attrs}>${escapeText(text)}</${node.name}>`\n  }\n\n  const inner = children.map((c) => serializeXml(c, indent + 1)).join('\\n')\n  return `${pad}<${node.name}${attrs}>\\n${inner}\\n${pad}</${node.name}>`\n}\n\nexport function walkExpandable(\n  node: XmlNode,\n  path: string[],\n  depth: number,\n  max: number,\n  visit: (path: string[], node: XmlNode) => void,\n) {\n  if (depth >= max) return\n  if (isExpandable(node)) {\n    visit(path, node)\n    const counts = new Map<string, number>()\n    const totals = new Map<string, number>()\n    for (const c of node.children ?? []) {\n      if (c.type === 'element' && c.name) totals.set(c.name, (totals.get(c.name) ?? 0) + 1)\n    }\n    ;(node.children ?? []).forEach((child, i) => {\n      let segment: string\n      if (child.type === 'element' && child.name) {\n        const n = (counts.get(child.name) ?? 0) + 1\n        counts.set(child.name, n)\n        const total = totals.get(child.name) ?? 1\n        segment = total > 1 ? `${child.name}[${n}]` : child.name\n      } else if (child.type === 'comment') {\n        segment = `comment()[${i}]`\n      } else {\n        segment = `text()[${i}]`\n      }\n      walkExpandable(child, [...path, segment], depth + 1, max, visit)\n    })\n  }\n}\n\nexport function childEntries(node: XmlNode, path: string[]) {\n  const counts = new Map<string, number>()\n  const totals = new Map<string, number>()\n  for (const c of node.children ?? []) {\n    if (c.type === 'element' && c.name) totals.set(c.name, (totals.get(c.name) ?? 0) + 1)\n  }\n  return (node.children ?? []).map((child, i) => {\n    let segment: string\n    if (child.type === 'element' && child.name) {\n      const n = (counts.get(child.name) ?? 0) + 1\n      counts.set(child.name, n)\n      const total = totals.get(child.name) ?? 1\n      segment = total > 1 ? `${child.name}[${n}]` : child.name\n    } else if (child.type === 'comment') {\n      segment = `comment()[${i}]`\n    } else {\n      segment = `text()[${i}]`\n    }\n    return { child, segment, path: [...path, segment] as string[] }\n  })\n}\n\nfunction domToNode(node: Node): XmlNode | null {\n  if (node.nodeType === Node.ELEMENT_NODE) {\n    const el = node as Element\n    const attributes: Record<string, string> = {}\n    const attrs: XmlAttr[] = []\n    for (let i = 0; i < el.attributes.length; i++) {\n      const a = el.attributes[i]\n      attributes[a.name] = a.value\n      attrs.push({ name: a.name, value: a.value })\n    }\n    const children: XmlNode[] = []\n    for (let i = 0; i < el.childNodes.length; i++) {\n      const n = domToNode(el.childNodes[i])\n      if (n) children.push(n)\n    }\n    return {\n      type: 'element',\n      name: el.tagName,\n      attributes,\n      attrs,\n      text: '',\n      value: '',\n      children,\n    }\n  }\n\n  if (node.nodeType === Node.TEXT_NODE) {\n    const text = node.textContent ?? ''\n    if (!text.trim()) return null\n    return { type: 'text', name: '', attributes: {}, attrs: [], text, value: text, children: [] }\n  }\n\n  if (node.nodeType === Node.COMMENT_NODE) {\n    const text = node.textContent ?? ''\n    return { type: 'comment', name: '', attributes: {}, attrs: [], text, value: text, children: [] }\n  }\n\n  if (node.nodeType === Node.CDATA_SECTION_NODE) {\n    const text = node.textContent ?? ''\n    return { type: 'cdata', name: '', attributes: {}, attrs: [], text, value: text, children: [] }\n  }\n\n  return null\n}\n\nexport function parseXml(input: string): { root: XmlNode | null; error: string | null } {\n  const src = input?.trim() ?? ''\n  if (!src) return { root: null, error: 'Empty XML' }\n\n  if (typeof DOMParser !== 'undefined') {\n    try {\n      const doc = new DOMParser().parseFromString(src, 'application/xml')\n      const parseError = doc.querySelector('parsererror')\n      if (parseError) {\n        const msg = parseError.textContent?.replace(/\\s+/g, ' ').trim() || 'Invalid XML'\n        return { root: null, error: msg }\n      }\n      const el = doc.documentElement\n      if (!el) return { root: null, error: 'Empty document' }\n      const root = domToNode(el)\n      if (!root) return { root: null, error: 'Could not read document element' }\n      return { root, error: null }\n    } catch (e) {\n      return { root: null, error: e instanceof Error ? e.message : 'Failed to parse XML' }\n    }\n  }\n\n  // Fallback lexer parser for Node / non-DOM environments\n  let pos = 0\n  const stack: XmlNode[] = []\n  let root: XmlNode | null = null\n\n  const attach = (n: XmlNode) => {\n    if (stack.length) {\n      stack[stack.length - 1]!.children!.push(n)\n    } else if (!root) {\n      root = n\n    }\n  }\n\n  while (pos < src.length) {\n    if (src.startsWith('<!--', pos)) {\n      const end = src.indexOf('-->', pos + 4)\n      const value = end === -1 ? src.slice(pos + 4) : src.slice(pos + 4, end)\n      attach({ type: 'comment', value, text: value, attributes: {}, attrs: [], children: [] })\n      pos = end === -1 ? src.length : end + 3\n    } else if (src.startsWith('<![CDATA[', pos)) {\n      const end = src.indexOf(']]>', pos + 9)\n      const value = end === -1 ? src.slice(pos + 9) : src.slice(pos + 9, end)\n      attach({ type: 'cdata', value, text: value, attributes: {}, attrs: [], children: [] })\n      pos = end === -1 ? src.length : end + 3\n    } else if (src[pos] === '<') {\n      const end = src.indexOf('>', pos + 1)\n      if (end === -1) {\n        return { root: null, error: 'Unclosed tag' }\n      }\n      const raw = src.slice(pos + 1, end).trim()\n      pos = end + 1\n      if (!raw || raw.startsWith('?') || raw.startsWith('!')) continue\n      if (raw.startsWith('/')) {\n        if (!stack.length) return { root: null, error: 'Unexpected closing tag' }\n        stack.pop()\n        continue\n      }\n      const selfClose = raw.endsWith('/')\n      const body = selfClose ? raw.slice(0, -1).trim() : raw\n      const parts = body.split(/\\s+/)\n      const name = parts[0] ?? 'node'\n      const attributes: Record<string, string> = {}\n      const attrs: XmlAttr[] = []\n      const attrRe = /([\\w:.-]+)\\s*=\\s*(\"([^\"]*)\"|'([^']*)')/g\n      let m: RegExpExecArray | null\n      while ((m = attrRe.exec(body)) !== null) {\n        const k = m[1]!\n        const v = m[3] ?? m[4] ?? ''\n        attributes[k] = v\n        attrs.push({ name: k, value: v })\n      }\n      const el: XmlNode = { type: 'element', name, attributes, attrs, children: [] }\n      attach(el)\n      if (!selfClose) stack.push(el)\n    } else {\n      const next = src.indexOf('<', pos)\n      const value = (next === -1 ? src.slice(pos) : src.slice(pos, next)).trim()\n      pos = next === -1 ? src.length : next\n      if (value) attach({ type: 'text', value, text: value, attributes: {}, attrs: [], children: [] })\n    }\n  }\n\n  if (stack.length > 0) {\n    return { root: null, error: 'Unclosed tag: ' + stack[stack.length - 1]?.name }\n  }\n\n  return { root, error: null }\n}\n\nconst tagColor = 'text-violet-600 dark:text-violet-400'\nconst attrNameColor = 'text-blue-600 dark:text-blue-400'\nconst attrValueColor = 'text-emerald-600 dark:text-emerald-400'\nconst textColor = 'text-emerald-600 dark:text-emerald-400'\nconst commentColor = 'text-muted-foreground'\nconst punctColor = 'text-muted-foreground'\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-xml-tree-node, [ui-xml-tree-node]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"xml-tree-node\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"treeitem\"',\n    '[attr.aria-expanded]': 'expandable ? open : null',\n    '[class]': 'hostClass',\n  },\n  template: `\n    @if (node) {\n      @if (node.type === 'element' && expandable) {\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)=\"toggle($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=\"size-3.5\"\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=\"size-3.5\"\n              >\n                <path d=\"m9 18 6-6-6-6\" />\n              </svg>\n            }\n          </button>\n          <span class=\"${punctColor} select-none\">&lt;</span>\n          <span class=\"${tagColor} select-none\">{{ node.name }}</span>\n          @for (attr of attributeList; track attr.name) {\n            <span class=\"select-none\">&nbsp;</span>\n            <span class=\"${attrNameColor} select-none\">{{ attr.name }}</span>\n            <span class=\"${punctColor} select-none\">=</span>\n            <span class=\"${attrValueColor} select-none\">\"{{ attr.value }}\"</span>\n          }\n          <span class=\"${punctColor} select-none\">&gt;</span>\n          @if (open) {\n            <span class=\"text-muted-foreground ml-0.5 text-xs\">\n              {{ childCount }} {{ childCount === 1 ? 'child' : 'children' }}\n            </span>\n          } @else {\n            <span class=\"text-muted-foreground ml-1 truncate text-xs select-none\">{{ collapsedPreview }}</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 shrink-0 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\"\n            aria-label=\"Copy\"\n            tabindex=\"-1\"\n            (click)=\"handleCopy($event)\"\n          >\n            @if (isCopied) {\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=\"size-3 text-emerald-500\"\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=\"size-3\"\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.segment) {\n              <ui-xml-tree-node\n                [node]=\"entry.child\"\n                [path]=\"entry.path\"\n                [isRoot]=\"false\"\n                [tree]=\"tree\"\n                [depth]=\"depth + 1\"\n              />\n            }\n            <div class=\"flex items-center gap-0.5 py-0.5 select-none\" [style.padding-left.px]=\"indent\">\n              <span class=\"inline-flex size-4 shrink-0\"></span>\n              <span class=\"${punctColor}\">&lt;/</span>\n              <span class=\"${tagColor}\">{{ node.name }}</span>\n              <span class=\"${punctColor}\">&gt;</span>\n            </div>\n          </div>\n        }\n      }\n\n      @if (node.type === 'element' && !expandable) {\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          <span class=\"${punctColor} select-none\">&lt;</span>\n          <span class=\"${tagColor} select-none\">{{ node.name }}</span>\n          @for (attr of attributeList; track attr.name) {\n            <span class=\"select-none\">&nbsp;</span>\n            <span class=\"${attrNameColor} select-none\">{{ attr.name }}</span>\n            <span class=\"${punctColor} select-none\">=</span>\n            <span class=\"${attrValueColor} select-none\">\"{{ attr.value }}\"</span>\n          }\n          @if (textOnlyChild !== null) {\n            <span class=\"${punctColor} select-none\">&gt;</span>\n            <span class=\"${textColor} truncate\">{{ textOnlyChild }}</span>\n            <span class=\"${punctColor} shrink-0 select-none\">&lt;/</span>\n            <span class=\"${tagColor} shrink-0 select-none\">{{ node.name }}</span>\n            <span class=\"${punctColor} shrink-0 select-none\">&gt;</span>\n          } @else {\n            <span class=\"${punctColor} select-none\"> /&gt;</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 shrink-0 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\"\n            aria-label=\"Copy\"\n            tabindex=\"-1\"\n            (click)=\"handleCopy($event)\"\n          >\n            @if (isCopied) {\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=\"size-3 text-emerald-500\"\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=\"size-3\"\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      @if (node.type === 'comment') {\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          <span class=\"${commentColor} truncate italic select-none\">&lt;!--{{ node.text || node.value }}--&gt;</span>\n          <button\n            type=\"button\"\n            class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring ml-auto inline-flex size-5 shrink-0 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\"\n            aria-label=\"Copy\"\n            tabindex=\"-1\"\n            (click)=\"handleCopy($event)\"\n          >\n            @if (isCopied) {\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=\"size-3 text-emerald-500\"\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=\"size-3\"\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      @if (node.type === 'cdata') {\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          <span class=\"${punctColor} select-none\">&lt;![CDATA[</span>\n          <span class=\"${textColor} truncate\">{{ node.text || node.value }}</span>\n          <span class=\"${punctColor} shrink-0 select-none\">]]&gt;</span>\n          <button\n            type=\"button\"\n            class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring ml-auto inline-flex size-5 shrink-0 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\"\n            aria-label=\"Copy\"\n            tabindex=\"-1\"\n            (click)=\"handleCopy($event)\"\n          >\n            @if (isCopied) {\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=\"size-3 text-emerald-500\"\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=\"size-3\"\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      @if (node.type === 'text') {\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          <span class=\"${textColor} truncate\">{{ node.text || node.value }}</span>\n          <button\n            type=\"button\"\n            class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring ml-auto inline-flex size-5 shrink-0 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\"\n            aria-label=\"Copy\"\n            tabindex=\"-1\"\n            (click)=\"handleCopy($event)\"\n          >\n            @if (isCopied) {\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=\"size-3 text-emerald-500\"\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=\"size-3\"\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  `,\n})\nexport class UiXmlTreeNodeComponent {\n  @Input() node?: XmlNode\n  @Input() path: string[] = []\n  @Input({ transform: booleanAttribute }) isRoot = false\n  @Input() tree?: UiXmlTreeViewComponent\n  @Input() depth = 0\n  @Input('class') className?: string\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 open(): boolean {\n    return this.tree ? this.tree.isExpanded(this.path) : false\n  }\n\n  get expandable(): boolean {\n    return this.node ? isExpandable(this.node) : false\n  }\n\n  get isCopied(): boolean {\n    return this.tree?.copiedPath === this.key\n  }\n\n  get indent(): number {\n    return this.isRoot ? 0 : 20\n  }\n\n  get attributeList(): XmlAttr[] {\n    if (!this.node) return []\n    if (this.node.attrs) return this.node.attrs\n    return Object.entries(this.node.attributes ?? {}).map(([name, value]) => ({ name, value }))\n  }\n\n  get childCount(): number {\n    return (this.node?.children ?? []).filter((c) => c.type === 'element').length\n  }\n\n  get collapsedPreview(): string {\n    if (!this.node || this.open || !this.expandable) return ''\n    const tags = (this.node.children ?? []).filter((c) => c.type === 'element').slice(0, 3)\n    const parts = tags.map((c) => `<${c.name}${Object.keys(c.attributes ?? {}).length ? ' …' : ''}>`)\n    const suffix = this.childCount > 3 ? ' …' : ''\n    return parts.join(' ') + suffix\n  }\n\n  get textOnlyChild(): string | null {\n    if (\n      this.node?.type === 'element' &&\n      this.node.children?.length === 1 &&\n      (this.node.children[0].type === 'text' || (!this.node.children[0].type && this.node.children[0].value))\n    ) {\n      return this.node.children[0].text ?? this.node.children[0].value ?? ''\n    }\n    return null\n  }\n\n  get entries() {\n    return this.node ? childEntries(this.node, this.path) : []\n  }\n\n  get hostClass(): string {\n    const dimmed = this.tree && this.node && this.tree.search && !this.tree.matchesSearch(this.node)\n    return cn('block', dimmed && 'opacity-30', this.className)\n  }\n\n  indentStyle(): Record<string, string> {\n    return { 'padding-left': `${this.depth * 20}px` }\n  }\n\n  toggle(e?: MouseEvent): void {\n    e?.stopPropagation()\n    if (this.tree) {\n      this.tree.toggle(this.path)\n    }\n  }\n\n  handleCopy(e?: MouseEvent): void {\n    e?.stopPropagation()\n    if (!this.node || !this.tree) return\n    const val = this.node.type === 'element' ? serializeXml(this.node) : (this.node.text ?? this.node.value ?? '')\n    this.tree.copyNode(val, this.path)\n  }\n\n  handleRowKeydown(e: KeyboardEvent): void {\n    const target = e.currentTarget as HTMLElement\n\n    if (e.key === 'Enter' || e.key === ' ') {\n      e.preventDefault()\n      if (this.expandable) this.toggle()\n      else this.handleCopy()\n      return\n    }\n\n    if (e.key === 'ArrowRight') {\n      e.preventDefault()\n      if (this.expandable && !this.open) {\n        this.toggle()\n      } else if (this.expandable && this.open) {\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        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.expandable && this.open) {\n        this.toggle()\n      } else if (this.parentKey) {\n        const tree = target.closest('[role=\"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      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      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 ? Array.from(tree.querySelectorAll<HTMLElement>('[data-tree-row]')) : []\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 ? Array.from(tree.querySelectorAll<HTMLElement>('[data-tree-row]')) : []\n      rows[rows.length - 1]?.focus()\n    }\n  }\n}\n\n/**\n * Angular port of UIPKGE XmlTreeView. Collapsible XML inspector with\n * color-coded tags, click-to-copy, search filter, and expand/collapse-all.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-xml-tree-view, [ui-xml-tree-view]',\n  standalone: true,\n  imports: [UiXmlTreeNodeComponent],\n  host: {\n    '[attr.data-slot]': '\"xml-tree-view\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n  },\n  template: `\n    @if (showToolbar || showSearch) {\n      <div 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=\"text-muted-foreground size-4\"\n          >\n            <path d=\"m18 16 4-4-4-4\" />\n            <path d=\"m6 8-4 4 4 4\" />\n            <path d=\"m14.5 4-5 16\" />\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 && !parseError) {\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=\"text-muted-foreground absolute top-1/2 left-2 size-3.5 -translate-y-1/2\"\n              >\n                <circle cx=\"11\" cy=\"11\" r=\"8\" />\n                <path d=\"m21 21-4.3-4.3\" />\n              </svg>\n              <input\n                type=\"text\"\n                [value]=\"search\"\n                (input)=\"onSearchInput($event)\"\n                placeholder=\"Filter...\"\n                aria-label=\"Filter XML 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 && !parseError) {\n            <span class=\"text-muted-foreground text-xs\">\n              {{ searchMatchCount }} match{{ searchMatchCount === 1 ? '' : 'es' }}\n            </span>\n          }\n          @if (!parseError) {\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=\"size-4\"\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=\"size-4\"\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=\"m9 19 3-3 3 3\" />\n                <path d=\"m15 5-3 3-3-3\" />\n              </svg>\n            </button>\n          }\n        </div>\n      </div>\n    }\n\n    @if (parseError) {\n      <div class=\"text-destructive flex items-start gap-2 p-4 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=\"mt-0.5 size-4 shrink-0\"\n        >\n          <circle cx=\"12\" cy=\"12\" r=\"10\" />\n          <line x1=\"12\" x2=\"12\" y1=\"8\" y2=\"12\" />\n          <line x1=\"12\" x2=\"12.01\" y1=\"16\" y2=\"16\" />\n        </svg>\n        <div class=\"min-w-0\">\n          <p class=\"font-sans font-medium\">Invalid XML</p>\n          <p class=\"text-muted-foreground mt-1 font-mono text-xs break-words\">{{ parseError }}</p>\n        </div>\n      </div>\n    } @else if (root) {\n      <div class=\"min-h-0 flex-1 overflow-auto p-2\" role=\"tree\" [attr.aria-label]=\"effectiveRootLabel\">\n        <ui-xml-tree-node [node]=\"root\" [path]=\"[]\" [isRoot]=\"true\" [tree]=\"this\" [depth]=\"0\" />\n      </div>\n    }\n    <ng-content />\n  `,\n})\nexport class UiXmlTreeViewComponent implements OnInit, OnChanges {\n  @Input() data = ''\n  @Input() expandDepth = 1\n  @Input() maxDepth = 100\n  @Input({ transform: booleanAttribute }) showSearch = true\n  @Input({ transform: booleanAttribute }) showToolbar = true\n  @Input() rootLabel?: string\n  @Input('class') className?: string\n\n  @Output() copy = new EventEmitter<{ value: string; path: string }>()\n\n  expanded = new Set<string>()\n  search = ''\n  copiedPath: string | null = null\n\n  root: XmlNode | null = null\n  parseError: string | null = null\n\n  get hostClass(): string {\n    return cn('bg-background flex flex-col overflow-hidden rounded-lg border font-mono text-sm block', this.className)\n  }\n\n  get effectiveRootLabel(): string {\n    return this.rootLabel ?? this.root?.name ?? 'xml'\n  }\n\n  get summary(): string {\n    if (this.parseError) return 'Parse error'\n    if (!this.root) return 'Empty'\n    const n = countElements(this.root)\n    return `${this.root.name} · ${n} element${n === 1 ? '' : 's'}`\n  }\n\n  get searchMatchCount(): number {\n    if (!this.search || !this.root) return 0\n    let count = 0\n    const term = this.search.toLowerCase()\n    const walk = (n: XmlNode) => {\n      if (n.type === 'element') {\n        if (n.name?.toLowerCase().includes(term)) count++\n        for (const [k, v] of Object.entries(n.attributes ?? {})) {\n          if (k.toLowerCase().includes(term) || v.toLowerCase().includes(term)) count++\n        }\n        for (const c of n.children ?? []) walk(c)\n        return\n      }\n      const val = n.text ?? n.value ?? ''\n      if (val.toLowerCase().includes(term)) count++\n    }\n    walk(this.root)\n    return count\n  }\n\n  ngOnInit(): void {\n    this.reparse()\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (changes['data'] || changes['expandDepth']) {\n      this.reparse()\n    }\n  }\n\n  private reparse(): void {\n    const res = parseXml(this.data)\n    this.root = res.root\n    this.parseError = res.error\n    this.resetExpanded()\n  }\n\n  private resetExpanded(): void {\n    const next = new Set<string>()\n    if (this.root) {\n      walkExpandable(this.root, [], 0, this.expandDepth, (path) => {\n        next.add(pathKey(path))\n      })\n    }\n    this.expanded = next\n  }\n\n  parsedRoot(): XmlNode | null {\n    return this.root ?? parseXml(this.data).root\n  }\n\n  rootName(): string {\n    if (this.rootLabel) return this.rootLabel\n    return this.parsedRoot()?.name ?? 'root'\n  }\n\n  isExpanded(path: string[]): boolean {\n    return this.expanded.has(pathKey(path))\n  }\n\n  toggle(path: string[]): 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    const next = new Set<string>()\n    const root = this.parsedRoot()\n    if (root) {\n      walkExpandable(root, [], 0, this.maxDepth, (path) => {\n        next.add(pathKey(path))\n      })\n      if (root.name) {\n        next.add(pathKey([root.name]))\n      }\n    }\n    this.expanded = next\n  }\n\n  collapseAll(): void {\n    this.expanded = new Set()\n  }\n\n  onSearchInput(e: Event): void {\n    this.search = (e.target as HTMLInputElement).value\n    if (!this.search) {\n      this.resetExpanded()\n      return\n    }\n    const next = new Set<string>()\n    if (this.root) {\n      walkExpandable(this.root, [], 0, this.maxDepth, (path, node) => {\n        if (this.matchesSearch(node)) next.add(pathKey(path))\n      })\n    }\n    this.expanded = next\n  }\n\n  matchesSearch(node: XmlNode): boolean {\n    const q = this.search.trim().toLowerCase()\n    if (!q) return true\n    if (node.name?.toLowerCase().includes(q)) return true\n    const val = node.text ?? node.value ?? ''\n    if (val.toLowerCase().includes(q)) return true\n    return Object.entries(node.attributes ?? {}).some(\n      ([k, v]) => k.toLowerCase().includes(q) || v.toLowerCase().includes(q),\n    )\n  }\n\n  copyNode(value: string, path: string[]): void {\n    const p = pathKey(path)\n    this.copiedPath = p\n    this.copy.emit({ value, path: p })\n    try {\n      if (typeof navigator !== 'undefined' && navigator.clipboard) {\n        navigator.clipboard.writeText(value)\n      }\n    } catch {\n      // clipboard optional\n    }\n    setTimeout(() => {\n      if (this.copiedPath === p) this.copiedPath = null\n    }, 1200)\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/xml-tree-view/xml-tree-view.component.ts"
    },
    {
      "path": "packages/registry-angular/components/xml-tree-view/index.ts",
      "content": "export {\n  UiXmlTreeViewComponent,\n  UiXmlTreeNodeComponent,\n  parseXml,\n  pathKey,\n  isExpandable,\n  type XmlNode,\n} from './xml-tree-view.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/xml-tree-view/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Collapsible XML tree viewer with color-coded tags and attributes, click-to-copy, live search/filter, and expand/collapse-all controls. Parses XML strings into elements, text, comments, and CDATA.",
  "categories": [
    "display",
    "data"
  ]
}