{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "xml-tree-view",
  "title": "Xml Tree View",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/xml-tree-view/XmlTreeView.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  // `oncopy` here is the copy-notification callback (value, path) — it shadows the\n  // native clipboard-event prop, which consumers don't need on this root.\n  export interface XmlTreeViewProps extends Omit<HTMLAttributes<HTMLDivElement>, '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</script>\n\n<script lang=\"ts\">\n  import { AlertCircle, CodeXml, FoldVertical, Search, UnfoldVertical } from '@lucide/svelte'\n  import { cn } from '$lib/utils'\n  import XmlTreeNode from './XmlTreeNode.svelte'\n  import { countElements, isExpandable, parseXml, type XmlNode } from './types'\n\n  let {\n    data,\n    expandDepth = 1,\n    maxDepth = 100,\n    showSearch = true,\n    showToolbar = true,\n    rootLabel,\n    oncopy,\n    class: className,\n    ...restProps\n  }: XmlTreeViewProps = $props()\n\n  let expanded = $state<Set<string>>(new Set())\n  let search = $state('')\n  let copiedPath = $state<string | null>(null)\n\n  const parsed = $derived(parseXml(data))\n  const root = $derived(parsed.root)\n  const parseError = $derived(parsed.error)\n\n  function pathKey(path: string[]): string {\n    return path.length ? '/' + path.join('/') : '/'\n  }\n\n  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\n  function defaultExpanded(): Set<string> {\n    const next = new Set<string>()\n    const r = root\n    if (!r) return next\n    // Root is always at path [] with key \"/\"\n    walkExpandable(r, [], 0, expandDepth, (path) => {\n      next.add(pathKey(path))\n    })\n    return next\n  }\n\n  // Reset expansion when the document or depth changes (mirrors Vue's immediate watcher).\n  $effect(() => {\n    const _d = data\n    const _e = expandDepth\n    expanded = defaultExpanded()\n  })\n\n  function toggle(path: string[]) {\n    const key = pathKey(path)\n    const next = new Set(expanded)\n    if (next.has(key)) next.delete(key)\n    else next.add(key)\n    expanded = next\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 = root\n    if (!r) {\n      expanded = next\n      return\n    }\n    walkExpandable(r, [], 0, maxDepth, (path) => {\n      next.add(pathKey(path))\n    })\n    expanded = next\n  }\n\n  function collapseAll() {\n    expanded = 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  // Auto-expand nodes that contain search matches. Guarded to first-run so the\n  // mount pass keeps the default expansion (mirrors Vue's non-immediate watcher).\n  let searchPrimed = false\n  $effect(() => {\n    const q = search\n    if (!searchPrimed) {\n      searchPrimed = true\n      return\n    }\n    if (!q) {\n      expanded = defaultExpanded()\n      return\n    }\n    const next = new Set<string>()\n    const r = root\n    if (!r) {\n      expanded = next\n      return\n    }\n    walkExpandable(r, [], 0, maxDepth, (path, node) => {\n      if (matchesSearch(node)) next.add(pathKey(path))\n    })\n    expanded = next\n  })\n\n  const tagColor = 'text-violet-600 dark:text-violet-400'\n  const attrNameColor = 'text-blue-600 dark:text-blue-400'\n  const attrValueColor = 'text-emerald-600 dark:text-emerald-400'\n  const textColor = 'text-emerald-600 dark:text-emerald-400'\n  const commentColor = 'text-muted-foreground'\n  const punctColor = 'text-muted-foreground'\n\n  async function copyValue(value: string, path: string[]) {\n    const p = pathKey(path)\n    try {\n      await navigator.clipboard.writeText(value)\n      copiedPath = p\n      oncopy?.(value, p)\n      setTimeout(() => {\n        if (copiedPath === p) copiedPath = null\n      }, 1200)\n    } catch {\n      // clipboard unavailable\n    }\n  }\n\n  const effectiveRootLabel = $derived(rootLabel ?? root?.name ?? 'xml')\n\n  const summary = $derived.by(() => {\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  })\n\n  const searchMatchCount = $derived.by(() => {\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  })\n</script>\n\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', className)}\n  {...restProps}\n>\n  <!-- Toolbar -->\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        <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        {#if showSearch && !parseError}\n          <div class=\"relative\">\n            <Search class=\"text-muted-foreground absolute top-1/2 left-2 size-3.5 -translate-y-1/2\" />\n            <input\n              bind:value={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        {/if}\n        {#if search && !parseError}\n          <span class=\"text-muted-foreground text-xs\">\n            {searchMatchCount} match{searchMatchCount === 1 ? '' : 'es'}\n          </span>\n        {/if}\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            onclick={expandAll}\n          >\n            <UnfoldVertical class=\"size-4\" />\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            onclick={collapseAll}\n          >\n            <FoldVertical class=\"size-4\" />\n          </button>\n        {/if}\n      </div>\n    </div>\n  {/if}\n\n  <!-- Parse error -->\n  {#if parseError}\n    <div 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  {:else if root}\n    <!-- Tree -->\n    <div class=\"min-h-0 flex-1 overflow-auto p-2\" role=\"tree\" aria-label={effectiveRootLabel}>\n      <XmlTreeNode\n        node={root}\n        path={[]}\n        isRoot={true}\n        {search}\n        {maxDepth}\n        {matchesSearch}\n        {isExpanded}\n        {toggle}\n        {tagColor}\n        {attrNameColor}\n        {attrValueColor}\n        {textColor}\n        {commentColor}\n        {punctColor}\n        {copiedPath}\n        oncopy={copyValue}\n      />\n    </div>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/xml-tree-view/XmlTreeView.svelte"
    },
    {
      "path": "packages/registry-svelte/components/xml-tree-view/XmlTreeNode.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { XmlNode } from './types'\n\n  export 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</script>\n\n<script lang=\"ts\">\n  import { Check, ChevronDown, ChevronRight, Copy } from '@lucide/svelte'\n  import { isExpandable, serializeXml } from './types'\n  import XmlTreeNodeSelf from './XmlTreeNode.svelte'\n\n  let {\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  }: XmlTreeNodeProps = $props()\n\n  function pathKey(p: string[]): string {\n    return p.length ? '/' + p.join('/') : '/'\n  }\n\n  const key = $derived(pathKey(path))\n  const open = $derived(isExpanded(path))\n  const expandable = $derived(isExpandable(node))\n  const dimmed = $derived(!!search && !matchesSearch(node))\n  const indent = $derived(isRoot ? 0 : 20)\n\n  const parentKey = $derived(path.length ? pathKey(path.slice(0, -1)) : null)\n\n  /** Child path segments with sibling indices for duplicate tag names. */\n  const childEntries = $derived.by(() => {\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') {\n        totals.set(c.name, (totals.get(c.name) ?? 0) + 1)\n      }\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 if (child.type === 'cdata') {\n        segment = `text()[${i}]`\n      } else {\n        segment = `text()[${i}]`\n      }\n      return { child, segment, path: [...path, segment] as string[] }\n    })\n  })\n\n  const childCount = $derived(node.children.filter((c) => c.type === 'element').length)\n\n  const collapsedPreview = $derived.by(() => {\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  })\n\n  const textOnlyChild = $derived.by(() => {\n    if (node.type !== 'element') return null\n    if (node.children.length === 1 && node.children[0]!.type === 'text') {\n      return node.children[0]!.text\n    }\n    return null\n  })\n\n  function onCopy() {\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: 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 onCopy()\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 rowClass =\n    '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  const copyBtnClass =\n    '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</script>\n\n<div\n  data-dimmed={dimmed ? '' : undefined}\n  class={dimmed ? 'opacity-30' : ''}\n  role=\"treeitem\"\n  aria-expanded={expandable ? open : undefined}\n  aria-selected={false}\n>\n  {#if node.type === 'element'}\n    <!-- Expandable element header -->\n    {#if expandable}\n      <!-- svelte-ignore a11y_no_static_element_interactions -->\n      <div\n        data-tree-row\n        data-tree-id={key}\n        data-tree-parent={parentKey ?? undefined}\n        tabindex=\"0\"\n        class={rowClass}\n        style:padding-left={`${indent}px`}\n        onclick={() => toggle(path)}\n        onkeydown={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          onclick={(e) => {\n            e.stopPropagation()\n            toggle(path)\n          }}\n        >\n          {#if open}\n            <ChevronDown class=\"size-3.5\" />\n          {:else}\n            <ChevronRight class=\"size-3.5\" />\n          {/if}\n        </button>\n        <span class={punctColor + ' select-none'}>&lt;</span>\n        <span class={tagColor + ' select-none'}>{node.name}</span>\n        {#each node.attributes as attr (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        {/each}\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        {/if}\n        <button\n          type=\"button\"\n          class={copyBtnClass}\n          title=\"Copy subtree\"\n          aria-label=\"Copy subtree\"\n          tabindex=\"-1\"\n          onclick={(e) => {\n            e.stopPropagation()\n            onCopy()\n          }}\n        >\n          {#if copiedPath === key}\n            <Check class=\"size-3 text-emerald-500\" />\n          {:else}\n            <Copy class=\"size-3\" />\n          {/if}\n        </button>\n      </div>\n\n      <!-- Expandable children -->\n      {#if open}\n        <div role=\"group\">\n          {#each childEntries as entry (entry.segment)}\n            <XmlTreeNodeSelf\n              node={entry.child}\n              path={entry.path}\n              isRoot={false}\n              {search}\n              maxDepth={maxDepth}\n              {matchesSearch}\n              {isExpanded}\n              {toggle}\n              {tagColor}\n              {attrNameColor}\n              {attrValueColor}\n              {textColor}\n              {commentColor}\n              {punctColor}\n              {copiedPath}\n              {oncopy}\n            />\n          {/each}\n          <div class=\"flex items-center gap-0.5 py-0.5 select-none\" style:padding-left={`${indent}px`}>\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      {/if}\n    {:else}\n      <!-- Inline element: text-only or empty / self-closing -->\n      <!-- svelte-ignore a11y_no_static_element_interactions -->\n      <div\n        data-tree-row\n        data-tree-id={key}\n        data-tree-parent={parentKey ?? undefined}\n        tabindex=\"0\"\n        class={rowClass}\n        style:padding-left={`${indent}px`}\n        onclick={onCopy}\n        onkeydown={handleRowKeydown}\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        {#each node.attributes as attr (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        {/each}\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        {/if}\n        <button\n          type=\"button\"\n          class={copyBtnClass}\n          title=\"Copy value\"\n          aria-label=\"Copy value\"\n          tabindex=\"-1\"\n          onclick={(e) => {\n            e.stopPropagation()\n            onCopy()\n          }}\n        >\n          {#if copiedPath === key}\n            <Check class=\"size-3 text-emerald-500\" />\n          {:else}\n            <Copy class=\"size-3\" />\n          {/if}\n        </button>\n      </div>\n    {/if}\n  {:else if node.type === 'comment'}\n    <!-- svelte-ignore a11y_no_static_element_interactions -->\n    <div\n      data-tree-row\n      data-tree-id={key}\n      data-tree-parent={parentKey ?? undefined}\n      tabindex=\"0\"\n      class={rowClass}\n      style:padding-left={`${indent}px`}\n      onclick={onCopy}\n      onkeydown={handleRowKeydown}\n    >\n      <span class=\"inline-flex size-4 shrink-0\"></span>\n      <span class={commentColor + ' truncate italic select-none'}>&lt;!--{node.text}--&gt;</span>\n      <button\n        type=\"button\"\n        class={copyBtnClass}\n        title=\"Copy comment\"\n        aria-label=\"Copy comment\"\n        tabindex=\"-1\"\n        onclick={(e) => {\n          e.stopPropagation()\n          onCopy()\n        }}\n      >\n        {#if copiedPath === key}\n          <Check class=\"size-3 text-emerald-500\" />\n        {:else}\n          <Copy class=\"size-3\" />\n        {/if}\n      </button>\n    </div>\n  {:else if node.type === 'cdata'}\n    <!-- svelte-ignore a11y_no_static_element_interactions -->\n    <div\n      data-tree-row\n      data-tree-id={key}\n      data-tree-parent={parentKey ?? undefined}\n      tabindex=\"0\"\n      class={rowClass}\n      style:padding-left={`${indent}px`}\n      onclick={onCopy}\n      onkeydown={handleRowKeydown}\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}</span>\n      <span class={punctColor + ' shrink-0 select-none'}>]]&gt;</span>\n      <button\n        type=\"button\"\n        class={copyBtnClass}\n        title=\"Copy CDATA\"\n        aria-label=\"Copy CDATA\"\n        tabindex=\"-1\"\n        onclick={(e) => {\n          e.stopPropagation()\n          onCopy()\n        }}\n      >\n        {#if copiedPath === key}\n          <Check class=\"size-3 text-emerald-500\" />\n        {:else}\n          <Copy class=\"size-3\" />\n        {/if}\n      </button>\n    </div>\n  {:else}\n    <!-- Bare text (rare when not folded into parent) -->\n    <!-- svelte-ignore a11y_no_static_element_interactions -->\n    <div\n      data-tree-row\n      data-tree-id={key}\n      data-tree-parent={parentKey ?? undefined}\n      tabindex=\"0\"\n      class={rowClass}\n      style:padding-left={`${indent}px`}\n      onclick={onCopy}\n      onkeydown={handleRowKeydown}\n    >\n      <span class=\"inline-flex size-4 shrink-0\"></span>\n      <span class={textColor + ' truncate'}>{node.text}</span>\n      <button\n        type=\"button\"\n        class={copyBtnClass}\n        title=\"Copy text\"\n        aria-label=\"Copy text\"\n        tabindex=\"-1\"\n        onclick={(e) => {\n          e.stopPropagation()\n          onCopy()\n        }}\n      >\n        {#if copiedPath === key}\n          <Check class=\"size-3 text-emerald-500\" />\n        {:else}\n          <Copy class=\"size-3\" />\n        {/if}\n      </button>\n    </div>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/xml-tree-view/XmlTreeNode.svelte"
    },
    {
      "path": "packages/registry-svelte/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 ? ' ' + node.attributes.map((a) => `${a.name}=\"${escapeAttr(a.value)}\"`).join(' ') : ''\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": "~/src/lib/components/ui/xml-tree-view/types.ts"
    },
    {
      "path": "packages/registry-svelte/components/xml-tree-view/index.ts",
      "content": "export { default as XmlTreeView, type XmlTreeViewProps } from './XmlTreeView.svelte'\nexport { default as XmlTreeNode, type XmlTreeNodeProps } from './XmlTreeNode.svelte'\nexport type { XmlNode, XmlAttr, XmlNodeType, ParseXmlResult } from './types'\nexport { parseXml, serializeXml, isExpandable, countElements } from './types'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/xml-tree-view/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "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"
  ]
}