{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "json-tree-view",
  "title": "Json Tree View",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/json-tree-view/JsonTreeView.tsx",
      "content": "import * as React from 'react'\nimport { ChevronDown, ChevronRight, Search, Braces, Copy, Check, FoldVertical, UnfoldVertical } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { JsonTreeNode } from './JsonTreeNode'\nimport type { JsonValue } from './types'\n\nexport type { JsonValue } from './types'\n\nexport interface JsonTreeViewProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'data' | 'onCopy'> {\n  data: JsonValue\n  expandDepth?: number\n  maxDepth?: number\n  showSearch?: boolean\n  showToolbar?: boolean\n  showPath?: boolean\n  rootLabel?: string\n  onCopy?: (value: string, path: string) => void\n}\n\nfunction pathKey(path: (string | number)[]): string {\n  return path.length ? path.map((p) => (typeof p === 'number' ? `[${p}]` : `.${p}`)).join('') : '$'\n}\n\nfunction typeOf(val: JsonValue): 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null' {\n  if (val === null) return 'null'\n  if (Array.isArray(val)) return 'array'\n  return typeof val as 'object' | 'string' | 'number' | 'boolean'\n}\n\nfunction 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\nconst 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\nconst keyColor = 'text-violet-600 dark:text-violet-400'\n\nconst JsonTreeView = React.forwardRef<HTMLDivElement, JsonTreeViewProps>(function JsonTreeView(props, ref) {\n  const {\n    data,\n    expandDepth = 1,\n    maxDepth = 100,\n    showSearch = true,\n    showToolbar = true,\n    showPath = true,\n    rootLabel = 'root',\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  const [hoveredPath, setHoveredPath] = React.useState<string | null>(null)\n\n  const dataRef = React.useRef(data)\n  dataRef.current = data\n  const expandDepthRef = React.useRef(expandDepth)\n  expandDepthRef.current = expandDepth\n  const maxDepthRef = React.useRef(maxDepth)\n  maxDepthRef.current = maxDepth\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 >= expandDepthRef.current) 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(dataRef.current)\n    return next\n  }\n\n  // Reset expanded state when data or expandDepth changes\n  React.useEffect(() => {\n    setExpanded(defaultExpanded())\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [data, expandDepth])\n\n  function toggle(path: (string | number)[]) {\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 | 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 >= maxDepthRef.current) 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(dataRef.current)\n    setExpanded(next)\n  }\n\n  function collapseAll() {\n    setExpanded(new Set())\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')\n        return Object.entries(v).some(([k, value]) => k.toLowerCase().includes(term) || walk(value))\n      return false\n    }\n    return walk(val)\n  }\n\n  // Auto-expand nodes that contain search matches\n  React.useEffect(() => {\n    if (!search) {\n      setExpanded(defaultExpanded())\n      return\n    }\n    const next = new Set<string>()\n    const walk = (val: JsonValue, path: (string | number)[] = [], depth = 0) => {\n      if (depth >= maxDepthRef.current) 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(dataRef.current)\n    setExpanded(next)\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [search])\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      setCopiedPath(p)\n      onCopy?.(str, p)\n      setTimeout(() => {\n        setCopiedPath((cur) => (cur === p ? null : cur))\n      }, 1200)\n    } catch {\n      // clipboard unavailable\n    }\n  }\n\n  function onHover(path: string | null) {\n    setHoveredPath(path)\n  }\n\n  const displayPath = React.useMemo(() => {\n    if (!showPath || !hoveredPath) return ''\n    if (hoveredPath === '$') return rootLabel\n    return hoveredPath\n  }, [showPath, hoveredPath, rootLabel])\n\n  const summary = React.useMemo(() => {\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  }, [data])\n\n  const searchMatchCount = React.useMemo(() => {\n    if (!search) return 0\n    let count = 0\n    const term = search.toLowerCase()\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(term)) 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  }, [search, data])\n\n  return (\n    <div\n      ref={ref}\n      data-uipkge=\"\"\n      data-slot=\"json-tree-view\"\n      className={cn('bg-background rounded-lg border font-mono text-sm', className)}\n      {...rest}\n    >\n      {/* Toolbar */}\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            <Braces 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 && (\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 JSON 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 && searchMatchCount > 0 && (\n              <span className=\"text-muted-foreground text-xs\">{searchMatchCount} matches</span>\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          </div>\n        </div>\n      )}\n\n      {/* Path bar */}\n      {showPath && displayPath && (\n        <div className=\"border-border bg-muted/30 text-muted-foreground truncate border-b px-3 py-1 text-xs\">\n          {displayPath}\n        </div>\n      )}\n\n      {/* Tree */}\n      <div className=\"overflow-auto p-2\">\n        <JsonTreeNode\n          data={data}\n          path={[]}\n          label={rootLabel}\n          isRoot\n          search={search}\n          maxDepth={maxDepth}\n          matchesSearch={matchesSearch}\n          isExpanded={isExpanded}\n          toggle={toggle}\n          typeOf={typeOf}\n          formatValue={formatValue}\n          typeColor={typeColor}\n          keyColor={keyColor}\n          copiedPath={copiedPath}\n          onCopy={copyValue}\n          onHover={onHover}\n        />\n      </div>\n    </div>\n  )\n})\n\nJsonTreeView.displayName = 'JsonTreeView'\n\nexport { JsonTreeView }\n",
      "type": "registry:ui",
      "target": "~/components/ui/json-tree-view/JsonTreeView.tsx"
    },
    {
      "path": "packages/registry-react/components/json-tree-view/JsonTreeNode.tsx",
      "content": "import * as React from 'react'\nimport { ChevronDown, ChevronRight, Copy, Check } from 'lucide-react'\nimport type { JsonValue } from './types'\n\nexport 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) => 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null'\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  onHover?: (path: string | null) => void\n}\n\nfunction pathKey(path: (string | number)[]): string {\n  return path.length ? path.map((p) => (typeof p === 'number' ? `[${p}]` : `.${p}`)).join('') : '$'\n}\n\nfunction JsonTreeNode(props: JsonTreeNodeProps): React.ReactElement {\n  const {\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    onHover,\n  } = props\n\n  const key = pathKey(path)\n  const type = typeOf(data)\n  const open = isExpanded(path)\n  const isContainer = type === 'object' || type === 'array'\n  const dimmed = !!search && !matchesSearch(data)\n\n  const entries: [string | number, JsonValue][] = React.useMemo(() => {\n    if (Array.isArray(data)) return data.map((v, i) => [i, v] as [number, JsonValue])\n    if (data !== null && typeof data === 'object') return Object.entries(data) as [string, JsonValue][]\n    return []\n  }, [data])\n\n  const count = entries.length\n  const indent = isRoot ? 0 : 20\n\n  // Collapsed preview: show first few items inline\n  const collapsedPreview = React.useMemo(() => {\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 open2 = type === 'array' ? '[' : '{'\n    const close = type === 'array' ? ']' : '}'\n    return `${open2}${parts.join(', ')}${suffix}${close}`\n  }, [open, isContainer, entries, count, type, typeOf, formatValue, data])\n\n  function handleCopy() {\n    onCopy?.(data, path)\n  }\n\n  function handleHover() {\n    onHover?.(key)\n  }\n\n  function handleLeave() {\n    onHover?.(null)\n  }\n\n  return (\n    <div data-dimmed={dimmed ? '' : undefined} className={dimmed ? 'opacity-30' : ''}>\n      {/* Container header row (object/array) */}\n      {isContainer && (\n        <div\n          className=\"group hover:bg-accent/40 -mx-1 flex items-center gap-0.5 rounded px-1 py-0.5 transition-colors\"\n          style={{ paddingLeft: `${indent}px` }}\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            onClick={() => toggle(path)}\n          >\n            {open ? <ChevronDown className=\"size-3.5\" /> : <ChevronRight className=\"size-3.5\" />}\n          </button>\n          <span className={`${keyColor} select-none`} onMouseEnter={handleHover} onMouseLeave={handleLeave}>\n            {isRoot ? label : `\"${label}\"`}\n          </span>\n          <span className=\"text-muted-foreground\">:</span>\n          {open ? (\n            <span className=\"text-muted-foreground select-none\">{type === 'array' ? '[' : '{'}</span>\n          ) : (\n            <span className=\"text-muted-foreground select-none\">{collapsedPreview}</span>\n          )}\n          {open && (\n            <span className=\"text-muted-foreground ml-0.5 text-xs\">\n              {count} {count === 1 ? 'item' : 'items'}\n            </span>\n          )}\n          <button\n            type=\"button\"\n            className=\"text-muted-foreground hover:text-foreground ml-auto inline-flex size-5 items-center justify-center rounded opacity-0 transition-opacity group-hover:opacity-100\"\n            title=\"Copy value\"\n            aria-label=\"Copy value\"\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        </div>\n      )}\n\n      {/* Container children */}\n      {isContainer && open && (\n        <>\n          {entries.map(([k, v]) => (\n            <JsonTreeNode\n              key={String(k)}\n              data={v}\n              path={[...path, k]}\n              label={String(k)}\n              isRoot={false}\n              search={search}\n              maxDepth={maxDepth}\n              matchesSearch={matchesSearch}\n              isExpanded={isExpanded}\n              toggle={toggle}\n              typeOf={typeOf}\n              formatValue={formatValue}\n              typeColor={typeColor}\n              keyColor={keyColor}\n              copiedPath={copiedPath}\n              onCopy={onCopy}\n              onHover={onHover}\n            />\n          ))}\n          <div className=\"text-muted-foreground py-0.5 select-none\" style={{ paddingLeft: `${indent}px` }}>\n            {type === 'array' ? ']' : '}'}\n          </div>\n        </>\n      )}\n\n      {/* Primitive leaf */}\n      {!isContainer && (\n        <div\n          className=\"group hover:bg-accent/40 -mx-1 flex items-center gap-0.5 rounded px-1 py-0.5 transition-colors\"\n          style={{ paddingLeft: `${indent}px` }}\n        >\n          <span className=\"inline-flex size-4 shrink-0\" />\n          {isRoot ? (\n            <span className=\"text-muted-foreground select-none\">{label}</span>\n          ) : (\n            <span className={`${keyColor} select-none`}>\"{label}\"</span>\n          )}\n          <span className=\"text-muted-foreground\">:</span>\n          <span\n            className={`${typeColor[type] ?? 'text-foreground'} cursor-pointer`}\n            onClick={handleCopy}\n            onMouseEnter={handleHover}\n            onMouseLeave={handleLeave}\n          >\n            {formatValue(data)}\n          </span>\n          <button\n            type=\"button\"\n            className=\"text-muted-foreground hover:text-foreground ml-auto inline-flex size-5 items-center justify-center rounded opacity-0 transition-opacity group-hover:opacity-100\"\n            title=\"Copy value\"\n            aria-label=\"Copy value\"\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        </div>\n      )}\n    </div>\n  )\n}\n\nexport { JsonTreeNode }\n",
      "type": "registry:ui",
      "target": "~/components/ui/json-tree-view/JsonTreeNode.tsx"
    },
    {
      "path": "packages/registry-react/components/json-tree-view/types.ts",
      "content": "export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }\n",
      "type": "registry:ui",
      "target": "~/components/ui/json-tree-view/types.ts"
    },
    {
      "path": "packages/registry-react/components/json-tree-view/index.ts",
      "content": "export { JsonTreeView, type JsonTreeViewProps } from './JsonTreeView'\nexport { JsonTreeNode, type JsonTreeNodeProps } from './JsonTreeNode'\nexport type { JsonValue } from './types'\n",
      "type": "registry:ui",
      "target": "~/components/ui/json-tree-view/index.ts"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Collapsible JSON tree viewer with color-coded value types, click-to-copy, live search/filter, hover path display, and expand/collapse-all controls. Renders objects, arrays, and primitives with configurable default depth and max depth.",
  "categories": [
    "display",
    "data"
  ]
}