{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "xml-tree-view",
  "title": "Xml Tree View",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/xml-tree-view/XmlTreeView.tsx",
      "content": "import * as React from 'react'\nimport { Search, CodeXml, FoldVertical, UnfoldVertical, AlertCircle } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { XmlTreeNode } from './XmlTreeNode'\nimport { parseXml, countElements, pathKey, walkExpandable, type XmlNode } from './types'\n\nexport type { XmlNode } from './types'\n\nexport interface XmlTreeViewProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'data' | 'onCopy'> {\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  onCopy?: (value: string, path: string) => void\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\nconst XmlTreeView = React.forwardRef<HTMLDivElement, XmlTreeViewProps>(function XmlTreeView(props, ref) {\n  const {\n    data,\n    expandDepth = 1,\n    maxDepth = 100,\n    showSearch = true,\n    showToolbar = true,\n    rootLabel,\n    onCopy,\n    className,\n    ...rest\n  } = props\n\n  const [expanded, setExpanded] = React.useState<Set<string>>(() => new Set())\n  const [search, setSearch] = React.useState('')\n  const [copiedPath, setCopiedPath] = React.useState<string | null>(null)\n\n  const parsed = React.useMemo(() => parseXml(data), [data])\n  const root = parsed.root\n  const parseError = parsed.error\n\n  const expandDepthRef = React.useRef(expandDepth)\n  expandDepthRef.current = expandDepth\n  const maxDepthRef = React.useRef(maxDepth)\n  maxDepthRef.current = maxDepth\n  const rootRef = React.useRef(root)\n  rootRef.current = root\n\n  function defaultExpanded(): Set<string> {\n    const next = new Set<string>()\n    const r = rootRef.current\n    if (!r) return next\n    walkExpandable(r, [], 0, expandDepthRef.current, (path) => {\n      next.add(pathKey(path))\n    })\n    return next\n  }\n\n  React.useEffect(() => {\n    setExpanded(defaultExpanded())\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [data, expandDepth])\n\n  function toggle(path: string[]) {\n    const k = pathKey(path)\n    setExpanded((prev) => {\n      const next = new Set(prev)\n      if (next.has(k)) next.delete(k)\n      else next.add(k)\n      return next\n    })\n  }\n\n  function isExpanded(path: string[]): boolean {\n    return expanded.has(pathKey(path))\n  }\n\n  function expandAll() {\n    const next = new Set<string>()\n    const r = rootRef.current\n    if (r) {\n      walkExpandable(r, [], 0, maxDepthRef.current, (path) => {\n        next.add(pathKey(path))\n      })\n    }\n    setExpanded(next)\n  }\n\n  function collapseAll() {\n    setExpanded(new Set())\n  }\n\n  function matchesSearch(node: XmlNode): boolean {\n    if (!search) return true\n    const term = search.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  React.useEffect(() => {\n    if (!search) {\n      setExpanded(defaultExpanded())\n      return\n    }\n    const next = new Set<string>()\n    const r = rootRef.current\n    if (r) {\n      walkExpandable(r, [], 0, maxDepthRef.current, (path, node) => {\n        if (matchesSearch(node)) next.add(pathKey(path))\n      })\n    }\n    setExpanded(next)\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [search])\n\n  async function copyValue(value: string, path: string[]) {\n    const p = pathKey(path)\n    try {\n      await navigator.clipboard.writeText(value)\n      setCopiedPath(p)\n      onCopy?.(value, p)\n      setTimeout(() => {\n        setCopiedPath((cur) => (cur === p ? null : cur))\n      }, 1200)\n    } catch {\n      // clipboard unavailable\n    }\n  }\n\n  const effectiveRootLabel = rootLabel ?? root?.name ?? 'xml'\n\n  const summary = React.useMemo(() => {\n    if (parseError) return 'Parse error'\n    if (!root) return 'Empty'\n    const n = countElements(root)\n    return `${root.name} · ${n} element${n === 1 ? '' : 's'}`\n  }, [parseError, root])\n\n  const searchMatchCount = React.useMemo(() => {\n    if (!search || !root) return 0\n    let count = 0\n    const term = search.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)\n    return count\n  }, [search, root])\n\n  return (\n    <div\n      ref={ref}\n      data-uipkge=\"\"\n      data-slot=\"xml-tree-view\"\n      className={cn('bg-background flex flex-col overflow-hidden rounded-lg border font-mono text-sm', className)}\n      {...rest}\n    >\n      {(showToolbar || showSearch) && (\n        <div className=\"border-border flex items-center gap-2 border-b px-3 py-2\">\n          <div className=\"flex items-center gap-1.5\">\n            <CodeXml className=\"text-muted-foreground size-4\" />\n            <span className=\"text-muted-foreground text-xs\">{summary}</span>\n          </div>\n          <div className=\"ml-auto flex items-center gap-1\">\n            {showSearch && !parseError && (\n              <div className=\"relative\">\n                <Search className=\"text-muted-foreground absolute top-1/2 left-2 size-3.5 -translate-y-1/2\" />\n                <input\n                  type=\"text\"\n                  value={search}\n                  onChange={(e) => setSearch(e.target.value)}\n                  placeholder=\"Filter...\"\n                  aria-label=\"Filter XML tree\"\n                  className=\"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            {search && !parseError && (\n              <span className=\"text-muted-foreground text-xs\">\n                {searchMatchCount} match{searchMatchCount === 1 ? '' : 'es'}\n              </span>\n            )}\n            {!parseError && (\n              <>\n                <button\n                  type=\"button\"\n                  className=\"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                  onClick={expandAll}\n                >\n                  <UnfoldVertical className=\"size-4\" />\n                </button>\n                <button\n                  type=\"button\"\n                  className=\"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                  onClick={collapseAll}\n                >\n                  <FoldVertical className=\"size-4\" />\n                </button>\n              </>\n            )}\n          </div>\n        </div>\n      )}\n\n      {parseError ? (\n        <div className=\"text-destructive flex items-start gap-2 p-4 text-sm\">\n          <AlertCircle className=\"mt-0.5 size-4 shrink-0\" />\n          <div className=\"min-w-0\">\n            <p className=\"font-sans font-medium\">Invalid XML</p>\n            <p className=\"text-muted-foreground mt-1 font-mono text-xs break-words\">{parseError}</p>\n          </div>\n        </div>\n      ) : root ? (\n        <div className=\"min-h-0 flex-1 overflow-auto p-2\" role=\"tree\" aria-label={effectiveRootLabel}>\n          <XmlTreeNode\n            node={root}\n            path={[]}\n            isRoot\n            search={search}\n            maxDepth={maxDepth}\n            matchesSearch={matchesSearch}\n            isExpanded={isExpanded}\n            toggle={toggle}\n            tagColor={tagColor}\n            attrNameColor={attrNameColor}\n            attrValueColor={attrValueColor}\n            textColor={textColor}\n            commentColor={commentColor}\n            punctColor={punctColor}\n            copiedPath={copiedPath}\n            onCopy={copyValue}\n          />\n        </div>\n      ) : null}\n    </div>\n  )\n})\n\nXmlTreeView.displayName = 'XmlTreeView'\n\nexport { XmlTreeView }\n",
      "type": "registry:ui",
      "target": "~/components/ui/xml-tree-view/XmlTreeView.tsx"
    },
    {
      "path": "packages/registry-react/components/xml-tree-view/XmlTreeNode.tsx",
      "content": "import * as React from 'react'\nimport { ChevronDown, ChevronRight, Copy, Check } from 'lucide-react'\nimport type { XmlNode } from './types'\nimport { isExpandable, serializeXml, pathKey, childEntries } from './types'\n\nexport interface XmlTreeNodeProps {\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  onCopy?: (value: string, path: string[]) => void\n}\n\nfunction XmlTreeNode(props: XmlTreeNodeProps): React.ReactElement {\n  const {\n    node,\n    path,\n    isRoot = false,\n    search = '',\n    maxDepth = 100,\n    matchesSearch,\n    isExpanded,\n    toggle,\n    tagColor,\n    attrNameColor,\n    attrValueColor,\n    textColor,\n    commentColor,\n    punctColor,\n    copiedPath = null,\n    onCopy,\n  } = props\n\n  const key = pathKey(path)\n  const open = isExpanded(path)\n  const expandable = isExpandable(node)\n  const dimmed = !!search && !matchesSearch(node)\n  const indent = isRoot ? 0 : 20\n  const parentKey = path.length ? pathKey(path.slice(0, -1)) : null\n\n  const entries = React.useMemo(() => childEntries(node, path), [node, path])\n  const childCount = node.children.filter((c) => c.type === 'element').length\n\n  const collapsedPreview = React.useMemo(() => {\n    if (open || !expandable) return ''\n    const tags = 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 > 3 ? ' …' : ''\n    return parts.join(' ') + suffix\n  }, [open, expandable, node.children, childCount])\n\n  const textOnlyChild =\n    node.type === 'element' && node.children.length === 1 && node.children[0]!.type === 'text'\n      ? node.children[0]!.text\n      : null\n\n  function handleCopy() {\n    const value = node.type === 'element' ? serializeXml(node) : node.text\n    onCopy?.(value, path)\n  }\n\n  function 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\n  function focusRow(row: HTMLElement | null | undefined) {\n    row?.focus()\n  }\n\n  function handleRowKeydown(e: React.KeyboardEvent) {\n    const target = e.currentTarget as HTMLElement\n\n    if (e.key === 'Enter' || e.key === ' ') {\n      e.preventDefault()\n      if (expandable) toggle(path)\n      else handleCopy()\n      return\n    }\n\n    if (e.key === 'ArrowRight') {\n      e.preventDefault()\n      if (expandable && !open) {\n        toggle(path)\n      } else if (expandable && open) {\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 && open) {\n        toggle(path)\n      } else if (parentKey) {\n        const tree = target.closest('[role=\"tree\"]')\n        const parent = tree?.querySelector<HTMLElement>(`[data-tree-row][data-tree-id=\"${CSS.escape(parentKey)}\"]`)\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\n  const copyBtn = (\n    <button\n      type=\"button\"\n      className=\"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      onClick={(e) => {\n        e.stopPropagation()\n        handleCopy()\n      }}\n    >\n      {copiedPath === key ? <Check className=\"size-3 text-emerald-500\" /> : <Copy className=\"size-3\" />}\n    </button>\n  )\n\n  const attrs = node.attributes.map((attr) => (\n    <React.Fragment key={attr.name}>\n      <span className=\"select-none\">&nbsp;</span>\n      <span className={`${attrNameColor} select-none`}>{attr.name}</span>\n      <span className={`${punctColor} select-none`}>=</span>\n      <span className={`${attrValueColor} select-none`}>\"{attr.value}\"</span>\n    </React.Fragment>\n  ))\n\n  return (\n    <div\n      data-dimmed={dimmed ? '' : undefined}\n      className={dimmed ? 'opacity-30' : ''}\n      role=\"treeitem\"\n      aria-expanded={expandable ? open : undefined}\n    >\n      {node.type === 'element' && expandable && (\n        <>\n          <div\n            data-tree-row\n            data-tree-id={key}\n            data-tree-parent={parentKey ?? undefined}\n            tabIndex={0}\n            className=\"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            onClick={() => toggle(path)}\n            onKeyDown={handleRowKeydown}\n          >\n            <button\n              type=\"button\"\n              className=\"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              onClick={(e) => {\n                e.stopPropagation()\n                toggle(path)\n              }}\n            >\n              {open ? <ChevronDown className=\"size-3.5\" /> : <ChevronRight className=\"size-3.5\" />}\n            </button>\n            <span className={`${punctColor} select-none`}>&lt;</span>\n            <span className={`${tagColor} select-none`}>{node.name}</span>\n            {attrs}\n            <span className={`${punctColor} select-none`}>&gt;</span>\n            {open ? (\n              <span className=\"text-muted-foreground ml-0.5 text-xs\">\n                {childCount} {childCount === 1 ? 'child' : 'children'}\n              </span>\n            ) : (\n              <span className=\"text-muted-foreground ml-1 truncate text-xs select-none\">{collapsedPreview}</span>\n            )}\n            {copyBtn}\n          </div>\n\n          {open && (\n            <div role=\"group\">\n              {entries.map((entry) => (\n                <XmlTreeNode\n                  key={entry.segment}\n                  node={entry.child}\n                  path={entry.path}\n                  isRoot={false}\n                  search={search}\n                  maxDepth={maxDepth}\n                  matchesSearch={matchesSearch}\n                  isExpanded={isExpanded}\n                  toggle={toggle}\n                  tagColor={tagColor}\n                  attrNameColor={attrNameColor}\n                  attrValueColor={attrValueColor}\n                  textColor={textColor}\n                  commentColor={commentColor}\n                  punctColor={punctColor}\n                  copiedPath={copiedPath}\n                  onCopy={onCopy}\n                />\n              ))}\n              <div className=\"flex items-center gap-0.5 py-0.5 select-none\" style={{ paddingLeft: `${indent}px` }}>\n                <span className=\"inline-flex size-4 shrink-0\" />\n                <span className={punctColor}>&lt;/</span>\n                <span className={tagColor}>{node.name}</span>\n                <span className={punctColor}>&gt;</span>\n              </div>\n            </div>\n          )}\n        </>\n      )}\n\n      {node.type === 'element' && !expandable && (\n        <div\n          data-tree-row\n          data-tree-id={key}\n          data-tree-parent={parentKey ?? undefined}\n          tabIndex={0}\n          className=\"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          onClick={handleCopy}\n          onKeyDown={handleRowKeydown}\n        >\n          <span className=\"inline-flex size-4 shrink-0\" />\n          <span className={`${punctColor} select-none`}>&lt;</span>\n          <span className={`${tagColor} select-none`}>{node.name}</span>\n          {attrs}\n          {textOnlyChild !== null ? (\n            <>\n              <span className={`${punctColor} select-none`}>&gt;</span>\n              <span className={`${textColor} truncate`}>{textOnlyChild}</span>\n              <span className={`${punctColor} shrink-0 select-none`}>&lt;/</span>\n              <span className={`${tagColor} shrink-0 select-none`}>{node.name}</span>\n              <span className={`${punctColor} shrink-0 select-none`}>&gt;</span>\n            </>\n          ) : (\n            <span className={`${punctColor} select-none`}> /&gt;</span>\n          )}\n          {copyBtn}\n        </div>\n      )}\n\n      {node.type === 'comment' && (\n        <div\n          data-tree-row\n          data-tree-id={key}\n          data-tree-parent={parentKey ?? undefined}\n          tabIndex={0}\n          className=\"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          onClick={handleCopy}\n          onKeyDown={handleRowKeydown}\n        >\n          <span className=\"inline-flex size-4 shrink-0\" />\n          <span className={`${commentColor} truncate italic select-none`}>&lt;!--{node.text}--&gt;</span>\n          {copyBtn}\n        </div>\n      )}\n\n      {node.type === 'cdata' && (\n        <div\n          data-tree-row\n          data-tree-id={key}\n          data-tree-parent={parentKey ?? undefined}\n          tabIndex={0}\n          className=\"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          onClick={handleCopy}\n          onKeyDown={handleRowKeydown}\n        >\n          <span className=\"inline-flex size-4 shrink-0\" />\n          <span className={`${punctColor} select-none`}>&lt;![CDATA[</span>\n          <span className={`${textColor} truncate`}>{node.text}</span>\n          <span className={`${punctColor} shrink-0 select-none`}>]]&gt;</span>\n          {copyBtn}\n        </div>\n      )}\n\n      {node.type === 'text' && (\n        <div\n          data-tree-row\n          data-tree-id={key}\n          data-tree-parent={parentKey ?? undefined}\n          tabIndex={0}\n          className=\"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          onClick={handleCopy}\n          onKeyDown={handleRowKeydown}\n        >\n          <span className=\"inline-flex size-4 shrink-0\" />\n          <span className={`${textColor} truncate`}>{node.text}</span>\n          {copyBtn}\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport { XmlTreeNode }\n",
      "type": "registry:ui",
      "target": "~/components/ui/xml-tree-view/XmlTreeNode.tsx"
    },
    {
      "path": "packages/registry-react/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\n/** Path key for expand/collapse set. */\nexport function pathKey(path: string[]): string {\n  return path.length ? '/' + path.join('/') : '/'\n}\n\n/** Walk expandable nodes with consistent sibling-indexed path segments. */\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') 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\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') 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') {\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",
      "type": "registry:ui",
      "target": "~/components/ui/xml-tree-view/types.ts"
    },
    {
      "path": "packages/registry-react/components/xml-tree-view/index.ts",
      "content": "export { XmlTreeView, type XmlTreeViewProps } from './XmlTreeView'\nexport { XmlTreeNode, type XmlTreeNodeProps } from './XmlTreeNode'\nexport type { XmlNode, XmlAttr, XmlNodeType, ParseXmlResult } from './types'\nexport { parseXml, serializeXml, isExpandable, countElements } from './types'\n",
      "type": "registry:ui",
      "target": "~/components/ui/xml-tree-view/index.ts"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "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"
  ]
}