{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "xml-tree-view",
  "title": "Xml Tree View",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/xml-tree-view/XmlTreeView.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { computed, ref, watch } from 'vue'\nimport { Search, CodeXml, FoldVertical, UnfoldVertical, AlertCircle } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport XmlTreeNode from './XmlTreeNode.vue'\nimport { parseXml, countElements, isExpandable, type XmlNode } from './types'\n\nexport type { XmlNode } from './types'\n\ninterface Props {\n  /** Raw XML string to parse and display. */\n  data: string\n  expandDepth?: number\n  maxDepth?: number\n  showSearch?: boolean\n  showToolbar?: boolean\n  /** Override path root label; defaults to the document element name. */\n  rootLabel?: string\n  class?: HTMLAttributes['class']\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  expandDepth: 1,\n  maxDepth: 100,\n  showSearch: true,\n  showToolbar: true,\n})\n\nconst emit = defineEmits<{\n  copy: [value: string, path: string]\n}>()\n\nconst expanded = ref<Set<string>>(new Set())\nconst search = ref('')\nconst copiedPath = ref<string | null>(null)\n\nconst parsed = computed(() => parseXml(props.data))\nconst root = computed(() => parsed.value.root)\nconst parseError = computed(() => parsed.value.error)\n\nfunction pathKey(path: string[]): string {\n  return path.length ? '/' + path.join('/') : '/'\n}\n\nfunction 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') 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') {\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\nfunction defaultExpanded(): Set<string> {\n  const next = new Set<string>()\n  const r = root.value\n  if (!r) return next\n  // Root is always at path [] with key \"/\"\n  walkExpandable(r, [], 0, props.expandDepth, (path) => {\n    next.add(pathKey(path))\n  })\n  return next\n}\n\nwatch(\n  () => [props.data, props.expandDepth],\n  () => {\n    expanded.value = defaultExpanded()\n  },\n  { immediate: true },\n)\n\nfunction toggle(path: string[]) {\n  const key = pathKey(path)\n  const next = new Set(expanded.value)\n  if (next.has(key)) next.delete(key)\n  else next.add(key)\n  expanded.value = next\n}\n\nfunction isExpanded(path: string[]): boolean {\n  return expanded.value.has(pathKey(path))\n}\n\nfunction expandAll() {\n  const next = new Set<string>()\n  const r = root.value\n  if (!r) {\n    expanded.value = next\n    return\n  }\n  walkExpandable(r, [], 0, props.maxDepth, (path) => {\n    next.add(pathKey(path))\n  })\n  expanded.value = next\n}\n\nfunction collapseAll() {\n  expanded.value = new Set()\n}\n\nfunction matchesSearch(node: XmlNode): boolean {\n  if (!search.value) return true\n  const term = search.value.toLowerCase()\n  const walk = (n: XmlNode): boolean => {\n    if (n.type === 'element') {\n      if (n.name.toLowerCase().includes(term)) return true\n      if (n.attributes.some((a) => a.name.toLowerCase().includes(term) || a.value.toLowerCase().includes(term)))\n        return true\n      return n.children.some(walk)\n    }\n    return n.text.toLowerCase().includes(term)\n  }\n  return walk(node)\n}\n\n// Auto-expand nodes that contain search matches\nwatch(search, (q) => {\n  if (!q) {\n    expanded.value = defaultExpanded()\n    return\n  }\n  const next = new Set<string>()\n  const r = root.value\n  if (!r) {\n    expanded.value = next\n    return\n  }\n  walkExpandable(r, [], 0, props.maxDepth, (path, node) => {\n    if (matchesSearch(node)) next.add(pathKey(path))\n  })\n  expanded.value = next\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\nasync function copyValue(value: string, path: string[]) {\n  const p = pathKey(path)\n  try {\n    await navigator.clipboard.writeText(value)\n    copiedPath.value = p\n    emit('copy', value, p)\n    setTimeout(() => {\n      if (copiedPath.value === p) copiedPath.value = null\n    }, 1200)\n  } catch {\n    // clipboard unavailable\n  }\n}\n\nconst effectiveRootLabel = computed(() => {\n  if (props.rootLabel) return props.rootLabel\n  return root.value?.name ?? 'xml'\n})\n\nconst summary = computed(() => {\n  if (parseError.value) return 'Parse error'\n  if (!root.value) return 'Empty'\n  const n = countElements(root.value)\n  return `${root.value.name} · ${n} element${n === 1 ? '' : 's'}`\n})\n\nconst searchMatchCount = computed(() => {\n  if (!search.value || !root.value) return 0\n  let count = 0\n  const term = search.value.toLowerCase()\n  const walk = (n: XmlNode) => {\n    if (n.type === 'element') {\n      if (n.name.toLowerCase().includes(term)) count++\n      for (const a of n.attributes) {\n        if (a.name.toLowerCase().includes(term) || a.value.toLowerCase().includes(term)) count++\n      }\n      n.children.forEach(walk)\n      return\n    }\n    if (n.text.toLowerCase().includes(term)) count++\n  }\n  walk(root.value)\n  return count\n})\n</script>\n\n<template>\n  <div\n    data-uipkge\n    data-slot=\"xml-tree-view\"\n    :class=\"cn('bg-background flex flex-col overflow-hidden rounded-lg border font-mono text-sm', props.class)\"\n  >\n    <!-- Toolbar -->\n    <div v-if=\"showToolbar || showSearch\" class=\"border-border flex items-center gap-2 border-b px-3 py-2\">\n      <div class=\"flex items-center gap-1.5\">\n        <CodeXml class=\"text-muted-foreground size-4\" />\n        <span class=\"text-muted-foreground text-xs\">{{ summary }}</span>\n      </div>\n      <div class=\"ml-auto flex items-center gap-1\">\n        <div v-if=\"showSearch && !parseError\" class=\"relative\">\n          <Search class=\"text-muted-foreground absolute top-1/2 left-2 size-3.5 -translate-y-1/2\" />\n          <input\n            v-model=\"search\"\n            type=\"text\"\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        <span v-if=\"search && !parseError\" class=\"text-muted-foreground text-xs\">\n          {{ searchMatchCount }} match{{ searchMatchCount === 1 ? '' : 'es' }}\n        </span>\n        <button\n          v-if=\"!parseError\"\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          <UnfoldVertical class=\"size-4\" />\n        </button>\n        <button\n          v-if=\"!parseError\"\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          <FoldVertical class=\"size-4\" />\n        </button>\n      </div>\n    </div>\n\n    <!-- Parse error -->\n    <div v-if=\"parseError\" class=\"text-destructive flex items-start gap-2 p-4 text-sm\">\n      <AlertCircle class=\"mt-0.5 size-4 shrink-0\" />\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\n    <!-- Tree -->\n    <div v-else-if=\"root\" class=\"min-h-0 flex-1 overflow-auto p-2\" role=\"tree\" :aria-label=\"effectiveRootLabel\">\n      <XmlTreeNode\n        :node=\"root\"\n        :path=\"[]\"\n        :is-root=\"true\"\n        :search=\"search\"\n        :max-depth=\"maxDepth\"\n        :matches-search=\"matchesSearch\"\n        :is-expanded=\"isExpanded\"\n        :toggle=\"toggle\"\n        :tag-color=\"tagColor\"\n        :attr-name-color=\"attrNameColor\"\n        :attr-value-color=\"attrValueColor\"\n        :text-color=\"textColor\"\n        :comment-color=\"commentColor\"\n        :punct-color=\"punctColor\"\n        :copied-path=\"copiedPath\"\n        @copy=\"copyValue\"\n      />\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/xml-tree-view/XmlTreeView.vue"
    },
    {
      "path": "packages/registry-vue/components/xml-tree-view/XmlTreeNode.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent } from 'vue'\nimport { ChevronDown, ChevronRight, Copy, Check } from 'lucide-vue-next'\nimport type { XmlNode } from './types'\nimport { isExpandable, serializeXml } from './types'\n\n// Self-reference for recursive rendering — use defineAsyncComponent to avoid circular import\nconst XmlTreeNode = defineAsyncComponent(() => import('./XmlTreeNode.vue'))\n\ninterface Props {\n  node: XmlNode\n  path: string[]\n  isRoot?: boolean\n  search?: string\n  maxDepth?: number\n  matchesSearch: (node: XmlNode) => boolean\n  isExpanded: (path: string[]) => boolean\n  toggle: (path: string[]) => void\n  tagColor: string\n  attrNameColor: string\n  attrValueColor: string\n  textColor: string\n  commentColor: string\n  punctColor: string\n  copiedPath?: string | null\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  isRoot: false,\n  search: '',\n  maxDepth: 100,\n  copiedPath: null,\n})\n\nconst emit = defineEmits<{\n  copy: [value: string, path: string[]]\n}>()\n\nfunction pathKey(path: string[]): string {\n  return path.length ? '/' + path.join('/') : '/'\n}\n\nconst key = computed(() => pathKey(props.path))\nconst open = computed(() => props.isExpanded(props.path))\nconst expandable = computed(() => isExpandable(props.node))\nconst dimmed = computed(() => !!props.search && !props.matchesSearch(props.node))\nconst indent = computed(() => (props.isRoot ? 0 : 20))\n\nconst parentKey = computed(() => {\n  if (!props.path.length) return null\n  return pathKey(props.path.slice(0, -1))\n})\n\n/** Child path segments with sibling indices for duplicate tag names. */\nconst childEntries = computed(() => {\n  const counts = new Map<string, number>()\n  const totals = new Map<string, number>()\n  for (const c of props.node.children) {\n    if (c.type === 'element') {\n      totals.set(c.name, (totals.get(c.name) ?? 0) + 1)\n    }\n  }\n  return props.node.children.map((child, i) => {\n    let segment: string\n    if (child.type === 'element') {\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 if (child.type === 'cdata') {\n      segment = `text()[${i}]`\n    } else {\n      segment = `text()[${i}]`\n    }\n    return { child, segment, path: [...props.path, segment] as string[] }\n  })\n})\n\nconst childCount = computed(() => props.node.children.filter((c) => c.type === 'element').length)\n\nconst collapsedPreview = computed(() => {\n  if (open.value || !expandable.value) return ''\n  const tags = props.node.children.filter((c) => c.type === 'element').slice(0, 3)\n  const parts = tags.map((c) => `<${c.name}${c.attributes.length ? ' …' : ''}>`)\n  const suffix = childCount.value > 3 ? ' …' : ''\n  return parts.join(' ') + suffix\n})\n\nconst textOnlyChild = computed(() => {\n  if (props.node.type !== 'element') return null\n  if (props.node.children.length === 1 && props.node.children[0]!.type === 'text') {\n    return props.node.children[0]!.text\n  }\n  return null\n})\n\nfunction onCopy() {\n  const value = props.node.type === 'element' ? serializeXml(props.node) : props.node.text\n  emit('copy', value, props.path)\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]'))\n}\n\nfunction focusRow(row: HTMLElement | null | undefined) {\n  row?.focus()\n}\n\nfunction handleRowKeydown(e: KeyboardEvent) {\n  const target = e.currentTarget as HTMLElement\n\n  if (e.key === 'Enter' || e.key === ' ') {\n    e.preventDefault()\n    if (expandable.value) props.toggle(props.path)\n    else onCopy()\n    return\n  }\n\n  if (e.key === 'ArrowRight') {\n    e.preventDefault()\n    if (expandable.value && !open.value) {\n      props.toggle(props.path)\n    } else if (expandable.value && open.value) {\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 (expandable.value && open.value) {\n      props.toggle(props.path)\n    } else if (parentKey.value) {\n      const tree = target.closest('[role=\"tree\"]')\n      const parent = tree?.querySelector<HTMLElement>(`[data-tree-row][data-tree-id=\"${CSS.escape(parentKey.value)}\"]`)\n      focusRow(parent)\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</script>\n\n<template>\n  <div\n    :data-dimmed=\"dimmed ? '' : undefined\"\n    :class=\"dimmed ? 'opacity-30' : ''\"\n    role=\"treeitem\"\n    :aria-expanded=\"expandable ? open : undefined\"\n  >\n    <!-- Element node -->\n    <template v-if=\"node.type === 'element'\">\n      <!-- Expandable element header -->\n      <div\n        v-if=\"expandable\"\n        data-tree-row\n        :data-tree-id=\"key\"\n        :data-tree-parent=\"parentKey ?? undefined\"\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=\"{ paddingLeft: `${indent}px` }\"\n        @click=\"toggle(path)\"\n        @keydown=\"handleRowKeydown\"\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          :aria-expanded=\"open\"\n          :aria-label=\"open ? 'Collapse' : 'Expand'\"\n          tabindex=\"-1\"\n          @click.stop=\"toggle(path)\"\n        >\n          <ChevronDown v-if=\"open\" class=\"size-3.5\" />\n          <ChevronRight v-else class=\"size-3.5\" />\n        </button>\n        <span :class=\"punctColor\" class=\"select-none\">&lt;</span>\n        <span :class=\"tagColor\" class=\"select-none\">{{ node.name }}</span>\n        <template v-for=\"attr in node.attributes\" :key=\"attr.name\">\n          <span class=\"select-none\">&nbsp;</span>\n          <span :class=\"attrNameColor\" class=\"select-none\">{{ attr.name }}</span>\n          <span :class=\"punctColor\" class=\"select-none\">=</span>\n          <span :class=\"attrValueColor\" class=\"select-none\">\"{{ attr.value }}\"</span>\n        </template>\n        <span :class=\"punctColor\" class=\"select-none\">&gt;</span>\n        <span v-if=\"open\" class=\"text-muted-foreground ml-0.5 text-xs\">\n          {{ childCount }} {{ childCount === 1 ? 'child' : 'children' }}\n        </span>\n        <span v-else class=\"text-muted-foreground ml-1 truncate text-xs select-none\">{{ collapsedPreview }}</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 subtree\"\n          aria-label=\"Copy subtree\"\n          tabindex=\"-1\"\n          @click.stop=\"onCopy\"\n        >\n          <Check v-if=\"copiedPath === key\" class=\"size-3 text-emerald-500\" />\n          <Copy v-else class=\"size-3\" />\n        </button>\n      </div>\n\n      <!-- Expandable children -->\n      <div v-if=\"expandable && open\" role=\"group\">\n        <XmlTreeNode\n          v-for=\"entry in childEntries\"\n          :key=\"entry.segment\"\n          :node=\"entry.child\"\n          :path=\"entry.path\"\n          :is-root=\"false\"\n          :search=\"search\"\n          :max-depth=\"maxDepth\"\n          :matches-search=\"matchesSearch\"\n          :is-expanded=\"isExpanded\"\n          :toggle=\"toggle\"\n          :tag-color=\"tagColor\"\n          :attr-name-color=\"attrNameColor\"\n          :attr-value-color=\"attrValueColor\"\n          :text-color=\"textColor\"\n          :comment-color=\"commentColor\"\n          :punct-color=\"punctColor\"\n          :copied-path=\"copiedPath\"\n          @copy=\"(val, p) => emit('copy', val, p)\"\n        />\n        <div class=\"flex items-center gap-0.5 py-0.5 select-none\" :style=\"{ paddingLeft: `${indent}px` }\">\n          <span class=\"inline-flex size-4 shrink-0\" />\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      <!-- Inline element: text-only or empty / self-closing -->\n      <div\n        v-if=\"!expandable\"\n        data-tree-row\n        :data-tree-id=\"key\"\n        :data-tree-parent=\"parentKey ?? undefined\"\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=\"{ paddingLeft: `${indent}px` }\"\n        @click=\"onCopy\"\n        @keydown=\"handleRowKeydown\"\n      >\n        <span class=\"inline-flex size-4 shrink-0\" />\n        <span :class=\"punctColor\" class=\"select-none\">&lt;</span>\n        <span :class=\"tagColor\" class=\"select-none\">{{ node.name }}</span>\n        <template v-for=\"attr in node.attributes\" :key=\"attr.name\">\n          <span class=\"select-none\">&nbsp;</span>\n          <span :class=\"attrNameColor\" class=\"select-none\">{{ attr.name }}</span>\n          <span :class=\"punctColor\" class=\"select-none\">=</span>\n          <span :class=\"attrValueColor\" class=\"select-none\">\"{{ attr.value }}\"</span>\n        </template>\n        <template v-if=\"textOnlyChild !== null\">\n          <span :class=\"punctColor\" class=\"select-none\">&gt;</span>\n          <span :class=\"textColor\" class=\"truncate\">{{ textOnlyChild }}</span>\n          <span :class=\"punctColor\" class=\"shrink-0 select-none\">&lt;/</span>\n          <span :class=\"tagColor\" class=\"shrink-0 select-none\">{{ node.name }}</span>\n          <span :class=\"punctColor\" class=\"shrink-0 select-none\">&gt;</span>\n        </template>\n        <template v-else>\n          <span :class=\"punctColor\" class=\"select-none\"> /&gt;</span>\n        </template>\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 value\"\n          aria-label=\"Copy value\"\n          tabindex=\"-1\"\n          @click.stop=\"onCopy\"\n        >\n          <Check v-if=\"copiedPath === key\" class=\"size-3 text-emerald-500\" />\n          <Copy v-else class=\"size-3\" />\n        </button>\n      </div>\n    </template>\n\n    <!-- Comment -->\n    <div\n      v-else-if=\"node.type === 'comment'\"\n      data-tree-row\n      :data-tree-id=\"key\"\n      :data-tree-parent=\"parentKey ?? undefined\"\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=\"{ paddingLeft: `${indent}px` }\"\n      @click=\"onCopy\"\n      @keydown=\"handleRowKeydown\"\n    >\n      <span class=\"inline-flex size-4 shrink-0\" />\n      <span :class=\"commentColor\" class=\"truncate italic select-none\">&lt;!--{{ node.text }}--&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 comment\"\n        aria-label=\"Copy comment\"\n        tabindex=\"-1\"\n        @click.stop=\"onCopy\"\n      >\n        <Check v-if=\"copiedPath === key\" class=\"size-3 text-emerald-500\" />\n        <Copy v-else class=\"size-3\" />\n      </button>\n    </div>\n\n    <!-- CDATA -->\n    <div\n      v-else-if=\"node.type === 'cdata'\"\n      data-tree-row\n      :data-tree-id=\"key\"\n      :data-tree-parent=\"parentKey ?? undefined\"\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=\"{ paddingLeft: `${indent}px` }\"\n      @click=\"onCopy\"\n      @keydown=\"handleRowKeydown\"\n    >\n      <span class=\"inline-flex size-4 shrink-0\" />\n      <span :class=\"punctColor\" class=\"select-none\">&lt;![CDATA[</span>\n      <span :class=\"textColor\" class=\"truncate\">{{ node.text }}</span>\n      <span :class=\"punctColor\" class=\"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 CDATA\"\n        aria-label=\"Copy CDATA\"\n        tabindex=\"-1\"\n        @click.stop=\"onCopy\"\n      >\n        <Check v-if=\"copiedPath === key\" class=\"size-3 text-emerald-500\" />\n        <Copy v-else class=\"size-3\" />\n      </button>\n    </div>\n\n    <!-- Bare text (rare when not folded into parent) -->\n    <div\n      v-else\n      data-tree-row\n      :data-tree-id=\"key\"\n      :data-tree-parent=\"parentKey ?? undefined\"\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=\"{ paddingLeft: `${indent}px` }\"\n      @click=\"onCopy\"\n      @keydown=\"handleRowKeydown\"\n    >\n      <span class=\"inline-flex size-4 shrink-0\" />\n      <span :class=\"textColor\" class=\"truncate\">{{ node.text }}</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 text\"\n        aria-label=\"Copy text\"\n        tabindex=\"-1\"\n        @click.stop=\"onCopy\"\n      >\n        <Check v-if=\"copiedPath === key\" class=\"size-3 text-emerald-500\" />\n        <Copy v-else class=\"size-3\" />\n      </button>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/xml-tree-view/XmlTreeNode.vue"
    },
    {
      "path": "packages/registry-vue/components/xml-tree-view/types.ts",
      "content": "export type XmlNodeType = 'element' | 'text' | 'comment' | 'cdata'\n\nexport interface XmlAttr {\n  name: string\n  value: string\n}\n\nexport interface XmlNode {\n  type: XmlNodeType\n  /** Tag name for elements; empty for text / comment / cdata. */\n  name: string\n  attributes: XmlAttr[]\n  /** Character data for text / comment / cdata nodes. */\n  text: string\n  children: XmlNode[]\n}\n\nexport interface ParseXmlResult {\n  root: XmlNode | null\n  error: string | null\n}\n\n/**\n * Convert a DOM node into a lightweight tree for rendering.\n * Whitespace-only text nodes are dropped so the tree stays readable.\n */\nfunction domToNode(node: Node): XmlNode | null {\n  if (node.nodeType === Node.ELEMENT_NODE) {\n    const el = node as Element\n    const attributes: XmlAttr[] = Array.from(el.attributes).map((a) => ({\n      name: a.name,\n      value: a.value,\n    }))\n    const children: XmlNode[] = []\n    for (const child of Array.from(el.childNodes)) {\n      const n = domToNode(child)\n      if (n) children.push(n)\n    }\n    return {\n      type: 'element',\n      name: el.tagName,\n      attributes,\n      text: '',\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: [], text, children: [] }\n  }\n\n  if (node.nodeType === Node.COMMENT_NODE) {\n    return {\n      type: 'comment',\n      name: '',\n      attributes: [],\n      text: node.textContent ?? '',\n      children: [],\n    }\n  }\n\n  if (node.nodeType === Node.CDATA_SECTION_NODE) {\n    return {\n      type: 'cdata',\n      name: '',\n      attributes: [],\n      text: node.textContent ?? '',\n      children: [],\n    }\n  }\n\n  return null\n}\n\n/**\n * Parse an XML string with the browser DOMParser.\n * Returns a structured tree or a human-readable error.\n */\nexport function parseXml(source: string): ParseXmlResult {\n  const trimmed = source?.trim() ?? ''\n  if (!trimmed) return { root: null, error: 'Empty XML' }\n\n  if (typeof DOMParser === 'undefined') {\n    return { root: null, error: 'DOMParser is not available in this environment' }\n  }\n\n  try {\n    const doc = new DOMParser().parseFromString(trimmed, '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/** Serialize a tree node (and descendants) back to XML. */\nexport function serializeXml(node: XmlNode, indent = 0): string {\n  const pad = '  '.repeat(indent)\n\n  if (node.type === 'text') return node.text\n  if (node.type === 'comment') return `${pad}<!--${node.text}-->`\n  if (node.type === 'cdata') return `${pad}<![CDATA[${node.text}]]>`\n\n  const attrs =\n    node.attributes.length > 0\n      ? ' ' + node.attributes.map((a) => `${a.name}=\"${escapeAttr(a.value)}\"`).join(' ')\n      : ''\n\n  if (node.children.length === 0) {\n    return `${pad}<${node.name}${attrs} />`\n  }\n\n  // Single text child → keep on one line\n  if (node.children.length === 1 && node.children[0]!.type === 'text') {\n    return `${pad}<${node.name}${attrs}>${escapeText(node.children[0]!.text)}</${node.name}>`\n  }\n\n  const inner = node.children.map((c) => serializeXml(c, indent + 1)).join('\\n')\n  return `${pad}<${node.name}${attrs}>\\n${inner}\\n${pad}</${node.name}>`\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\n/** True when an element expands (has non-trivial children). */\nexport function isExpandable(node: XmlNode): boolean {\n  if (node.type !== 'element') return false\n  if (node.children.length === 0) return false\n  // Single text child stays inline — no expand\n  if (node.children.length === 1 && node.children[0]!.type === 'text') return false\n  return true\n}\n\n/** Count element descendants (for summary). */\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",
      "type": "registry:ui",
      "target": "~/app/components/ui/xml-tree-view/types.ts"
    },
    {
      "path": "packages/registry-vue/components/xml-tree-view/index.ts",
      "content": "export { default as XmlTreeView } from './XmlTreeView.vue'\nexport { default as XmlTreeNode } from './XmlTreeNode.vue'\nexport type { XmlNode, XmlAttr, XmlNodeType, ParseXmlResult } from './types'\nexport { parseXml, serializeXml, isExpandable, countElements } from './types'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/xml-tree-view/index.ts"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "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 and renders elements, text, comments, and CDATA with contained scrolling.",
  "categories": [
    "display",
    "data"
  ]
}