{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "json-tree-view",
  "title": "Json Tree View",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/json-tree-view/JsonTreeView.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n  import type { JsonTreeType, JsonValue } from './types'\n\n  export type { JsonValue } from './types'\n\n  export interface JsonTreeViewProps extends Omit<HTMLAttributes<HTMLDivElement>, 'oncopy'> {\n    data: JsonValue\n    expandDepth?: number\n    maxDepth?: number\n    showSearch?: boolean\n    showToolbar?: boolean\n    rootLabel?: string\n    /** Fires after a value is copied: the copied string and its path key. */\n    oncopy?: (value: string, path: string) => void\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { Braces, FoldVertical, Search, UnfoldVertical } from '@lucide/svelte'\n  import { cn } from '$lib/utils'\n  import JsonTreeNode from './JsonTreeNode.svelte'\n\n  let {\n    class: className,\n    data,\n    expandDepth = 1,\n    maxDepth = 100,\n    showSearch = true,\n    showToolbar = true,\n    rootLabel = 'root',\n    oncopy,\n    ref = $bindable(null),\n    ...restProps\n  }: JsonTreeViewProps = $props()\n\n  let expanded = $state<Set<string>>(new Set())\n  let search = $state('')\n  let copiedPath = $state<string | null>(null)\n\n  function pathKey(path: (string | number)[]): string {\n    return path.length ? path.map((p) => (typeof p === 'number' ? `[${p}]` : `.${p}`)).join('') : '$'\n  }\n\n  function defaultExpanded(): Set<string> {\n    const next = new Set<string>()\n    const walk = (val: JsonValue, path: (string | number)[] = [], depth = 0) => {\n      if (depth >= expandDepth) return\n      if (val !== null && typeof val === 'object') {\n        next.add(pathKey(path))\n        const entries = Array.isArray(val) ? val.map((v, i) => [i, v] as const) : Object.entries(val)\n        for (const [k, v] of entries) {\n          walk(v as JsonValue, [...path, k], depth + 1)\n        }\n      }\n    }\n    walk(data)\n    return next\n  }\n\n  $effect(() => {\n    void data\n    void expandDepth\n    expanded = defaultExpanded()\n  })\n\n  function toggle(path: (string | number)[]) {\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 | number)[]): boolean {\n    return expanded.has(pathKey(path))\n  }\n\n  function expandAll() {\n    const next = new Set<string>()\n    const walk = (val: JsonValue, path: (string | number)[] = [], depth = 0) => {\n      if (depth >= maxDepth) return\n      if (val !== null && typeof val === 'object') {\n        next.add(pathKey(path))\n        const entries = Array.isArray(val) ? val.map((v, i) => [i, v] as const) : Object.entries(val)\n        for (const [k, v] of entries) {\n          walk(v as JsonValue, [...path, k], depth + 1)\n        }\n      }\n    }\n    walk(data)\n    expanded = next\n  }\n\n  function collapseAll() {\n    expanded = new Set()\n  }\n\n  // Auto-expand nodes that contain search matches\n  $effect(() => {\n    const q = search\n    if (!q) {\n      expanded = defaultExpanded()\n      return\n    }\n    const next = new Set<string>()\n    const walk = (val: JsonValue, path: (string | number)[] = [], depth = 0) => {\n      if (depth >= maxDepth) return\n      if (val !== null && typeof val === 'object') {\n        if (matchesSearch(val)) next.add(pathKey(path))\n        const entries = Array.isArray(val) ? val.map((v, i) => [i, v] as const) : Object.entries(val)\n        for (const [k, v] of entries) {\n          walk(v as JsonValue, [...path, k], depth + 1)\n        }\n      }\n    }\n    walk(data)\n    expanded = next\n  })\n\n  function matchesSearch(val: JsonValue): boolean {\n    if (!search) return true\n    const term = search.toLowerCase()\n    const walk = (v: JsonValue): boolean => {\n      if (v === null) return 'null'.includes(term)\n      if (typeof v === 'string') return v.toLowerCase().includes(term)\n      if (typeof v === 'number' || typeof v === 'boolean') return String(v).includes(term)\n      if (Array.isArray(v)) return v.some(walk)\n      if (typeof v === 'object') return Object.entries(v).some(([k, val]) => k.toLowerCase().includes(term) || walk(val))\n      return false\n    }\n    return walk(val)\n  }\n\n  function typeOf(val: JsonValue): JsonTreeType {\n    if (val === null) return 'null'\n    if (Array.isArray(val)) return 'array'\n    return typeof val as 'object' | 'string' | 'number' | 'boolean'\n  }\n\n  function formatValue(val: JsonValue): string {\n    if (val === null) return 'null'\n    if (typeof val === 'string') return JSON.stringify(val)\n    return String(val)\n  }\n\n  const typeColor: Record<string, string> = {\n    string: 'text-emerald-600 dark:text-emerald-400',\n    number: 'text-blue-600 dark:text-blue-400',\n    boolean: 'text-amber-600 dark:text-amber-400',\n    null: 'text-muted-foreground italic',\n    object: 'text-foreground',\n    array: 'text-foreground',\n  }\n\n  const keyColor = 'text-violet-600 dark:text-violet-400'\n\n  async function copyValue(val: JsonValue, path: (string | number)[]) {\n    const str = typeof val === 'string' ? val : JSON.stringify(val, null, 2)\n    const p = pathKey(path)\n    try {\n      await navigator.clipboard.writeText(str)\n      copiedPath = p\n      oncopy?.(str, p)\n      setTimeout(() => {\n        if (copiedPath === p) copiedPath = null\n      }, 1200)\n    } catch {\n      // clipboard unavailable\n    }\n  }\n\n  const summary = $derived.by(() => {\n    const t = typeOf(data)\n    if (t === 'array') return `Array(${(data as JsonValue[]).length})`\n    if (t === 'object') return `Object(${Object.keys(data as object).length})`\n    return t\n  })\n\n  const searchMatchCount = $derived.by(() => {\n    if (!search) return 0\n    const term = search.toLowerCase()\n    let count = 0\n    const walk = (v: JsonValue) => {\n      if (v === null) {\n        if ('null'.includes(term)) count++\n        return\n      }\n      if (typeof v === 'string') {\n        if (v.toLowerCase().includes(term)) count++\n        return\n      }\n      if (typeof v === 'number' || typeof v === 'boolean') {\n        if (String(v).includes(search)) count++\n        return\n      }\n      if (Array.isArray(v)) {\n        v.forEach(walk)\n        return\n      }\n      if (typeof v === 'object') {\n        Object.entries(v).forEach(([k, val]) => {\n          if (k.toLowerCase().includes(term)) count++\n          walk(val)\n        })\n      }\n    }\n    walk(data)\n    return count\n  })\n</script>\n\n<div\n  bind:this={ref}\n  data-uipkge=\"\"\n  data-slot=\"json-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        <Braces 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}\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 JSON tree\"\n              class=\"border-input bg-muted/40 focus:border-ring focus:ring-ring/30 h-7 w-32 rounded-md pr-2 pl-7 text-xs transition-[width] outline-none focus:w-44 focus:ring-2\"\n            />\n          </div>\n        {/if}\n        {#if search}\n          <span class=\"text-muted-foreground text-xs\">\n            {searchMatchCount} match{searchMatchCount === 1 ? '' : 'es'}\n          </span>\n        {/if}\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      </div>\n    </div>\n  {/if}\n\n  <!-- Tree -->\n  <div class=\"min-h-0 flex-1 overflow-auto p-2\" role=\"tree\" aria-label={rootLabel}>\n    <JsonTreeNode\n      {data}\n      path={[]}\n      label={rootLabel}\n      isRoot={true}\n      {search}\n      {maxDepth}\n      {matchesSearch}\n      {isExpanded}\n      {toggle}\n      {typeOf}\n      {formatValue}\n      {typeColor}\n      {keyColor}\n      {copiedPath}\n      oncopy={copyValue}\n    />\n  </div>\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/json-tree-view/JsonTreeView.svelte"
    },
    {
      "path": "packages/registry-svelte/components/json-tree-view/JsonTreeNode.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { JsonTreeType, JsonValue } from './types'\n\n  export interface JsonTreeNodeProps {\n    data: JsonValue\n    path: (string | number)[]\n    label: string\n    isRoot?: boolean\n    search?: string\n    maxDepth?: number\n    matchesSearch: (val: JsonValue) => boolean\n    isExpanded: (path: (string | number)[]) => boolean\n    toggle: (path: (string | number)[]) => void\n    typeOf: (val: JsonValue) => JsonTreeType\n    formatValue: (val: JsonValue) => string\n    typeColor: Record<string, string>\n    keyColor: string\n    copiedPath?: string | null\n    oncopy?: (value: JsonValue, path: (string | number)[]) => void\n  }\n</script>\n\n<script lang=\"ts\">\n  import { Check, ChevronDown, ChevronRight, Copy } from '@lucide/svelte'\n  // Self-reference for recursive rendering.\n  import JsonTreeNode from './JsonTreeNode.svelte'\n\n  let {\n    data,\n    path,\n    label,\n    isRoot = false,\n    search = '',\n    maxDepth = 100,\n    matchesSearch,\n    isExpanded,\n    toggle,\n    typeOf,\n    formatValue,\n    typeColor,\n    keyColor,\n    copiedPath = null,\n    oncopy,\n  }: JsonTreeNodeProps = $props()\n\n  function pathKey(p: (string | number)[]): string {\n    return p.length ? p.map((seg) => (typeof seg === 'number' ? `[${seg}]` : `.${seg}`)).join('') : '$'\n  }\n\n  const key = $derived(pathKey(path))\n  const type = $derived(typeOf(data))\n  const open = $derived(isExpanded(path))\n  const isContainer = $derived(type === 'object' || type === 'array')\n  const dimmed = $derived(!!search && !matchesSearch(data))\n\n  const entries = $derived.by((): [string | number, JsonValue][] => {\n    if (Array.isArray(data)) return data.map((v, i): [number, JsonValue] => [i, v])\n    if (data !== null && typeof data === 'object') return Object.entries(data) as [string, JsonValue][]\n    return []\n  })\n\n  const count = $derived(entries.length)\n  const indent = $derived(isRoot ? 0 : 20)\n\n  // Collapsed preview: show first few items inline\n  const collapsedPreview = $derived.by(() => {\n    if (open || !isContainer) return ''\n    const items = entries.slice(0, 3)\n    const parts = items.map(([k, v]) => {\n      const vt = typeOf(v)\n      let valStr: string\n      if (vt === 'string') valStr = `\"${String(v).slice(0, 20)}\"`\n      else if (vt === 'array') valStr = '[…]'\n      else if (vt === 'object') valStr = '{…}'\n      else valStr = formatValue(v)\n      return `${Array.isArray(data) ? '' : `\"${k}\": `}${valStr}`\n    })\n    const suffix = count > 3 ? ', …' : ''\n    const openBracket = type === 'array' ? '[' : '{'\n    const close = type === 'array' ? ']' : '}'\n    return `${openBracket}${parts.join(', ')}${suffix}${close}`\n  })\n\n  const parentKey = $derived(path.length ? pathKey(path.slice(0, -1)) : null)\n\n  function handleCopy() {\n    oncopy?.(data, 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 & { currentTarget: HTMLElement }) {\n    const target = e.currentTarget\n\n    if (e.key === 'Enter' || e.key === ' ') {\n      if (isContainer) {\n        e.preventDefault()\n        toggle(path)\n      } else {\n        e.preventDefault()\n        handleCopy()\n      }\n      return\n    }\n\n    if (e.key === 'ArrowRight') {\n      e.preventDefault()\n      if (isContainer && !open) {\n        toggle(path)\n      } else if (isContainer && 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 (isContainer && open) {\n        toggle(path)\n      } else if (parentKey) {\n        const tree = target.closest('[role=\"tree\"]')\n        const parent = tree?.querySelector<HTMLElement>(\n          `[data-tree-row][data-tree-id=\"${CSS.escape(parentKey)}\"]`,\n        )\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<!-- svelte-ignore a11y_role_has_required_aria_props: rows are navigable, not selectable — aria-selected would mislead -->\n<div\n  data-dimmed={dimmed ? '' : undefined}\n  class={dimmed ? 'opacity-30' : ''}\n  role=\"treeitem\"\n  aria-expanded={isContainer ? open : undefined}\n>\n  <!-- Container header row (object/array) -->\n  {#if isContainer}\n    <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_static_element_interactions: tree row with full roving keyboard support -->\n    <div\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=\"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=\"{keyColor} select-none\">\n        {isRoot ? label : `\"${label}\"`}\n      </span>\n      <span class=\"text-muted-foreground\">:</span>\n      {#if open}\n        <span class=\"text-muted-foreground select-none\">{type === 'array' ? '[' : '{'}</span>\n      {:else}\n        <span class=\"text-muted-foreground select-none\">{collapsedPreview}</span>\n      {/if}\n      {#if open}\n        <span class=\"text-muted-foreground ml-0.5 text-xs\">{count} {count === 1 ? 'item' : 'items'}</span>\n      {/if}\n      <button\n        type=\"button\"\n        class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring ml-auto inline-flex size-5 items-center justify-center rounded opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-1\"\n        title=\"Copy value\"\n        aria-label=\"Copy value\"\n        tabindex=\"-1\"\n        onclick={(e) => {\n          e.stopPropagation()\n          handleCopy()\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\n  <!-- Container children -->\n  {#if isContainer && open}\n    <div role=\"group\">\n      {#each entries as [k, v] (String(k))}\n        <JsonTreeNode\n          data={v}\n          path={[...path, k]}\n          label={String(k)}\n          isRoot={false}\n          {search}\n          {maxDepth}\n          {matchesSearch}\n          {isExpanded}\n          {toggle}\n          {typeOf}\n          {formatValue}\n          {typeColor}\n          {keyColor}\n          {copiedPath}\n          {oncopy}\n        />\n      {/each}\n      <div class=\"text-muted-foreground py-0.5 select-none\" style=\"padding-left: {indent}px\">\n        {type === 'array' ? ']' : '}'}\n      </div>\n    </div>\n  {/if}\n\n  <!-- Primitive leaf -->\n  {#if !isContainer}\n    <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_static_element_interactions: tree row with full roving keyboard support -->\n    <div\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=\"padding-left: {indent}px\"\n      onclick={handleCopy}\n      onkeydown={handleRowKeydown}\n    >\n      <span class=\"inline-flex size-4 shrink-0\"></span>\n      {#if isRoot}\n        <span class=\"text-muted-foreground select-none\">{label}</span>\n      {:else}\n        <span class=\"{keyColor} select-none\">\"{label}\"</span>\n      {/if}\n      <span class=\"text-muted-foreground\">:</span>\n      <span class=\"{typeColor[type] ?? 'text-foreground'} rounded text-left font-mono\">\n        {formatValue(data)}\n      </span>\n      <button\n        type=\"button\"\n        class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring ml-auto inline-flex size-5 items-center justify-center rounded opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-1\"\n        title=\"Copy value\"\n        aria-label=\"Copy value\"\n        tabindex=\"-1\"\n        onclick={(e) => {\n          e.stopPropagation()\n          handleCopy()\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/json-tree-view/JsonTreeNode.svelte"
    },
    {
      "path": "packages/registry-svelte/components/json-tree-view/types.ts",
      "content": "export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }\n\nexport type JsonTreeType = 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/json-tree-view/types.ts"
    },
    {
      "path": "packages/registry-svelte/components/json-tree-view/index.ts",
      "content": "export { default as JsonTreeView, type JsonTreeViewProps } from './JsonTreeView.svelte'\nexport { default as JsonTreeNode, type JsonTreeNodeProps } from './JsonTreeNode.svelte'\nexport type { JsonTreeType, JsonValue } from './types'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/json-tree-view/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Collapsible JSON tree viewer with color-coded value types, click-to-copy, live search/filter, and expand/collapse-all controls. Renders objects, arrays, and primitives with configurable depth and contained scrolling.",
  "categories": [
    "display",
    "data"
  ]
}