{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "json-inspector",
  "title": "JSON Inspector",
  "description": "A read-only JSON viewer with theme-aware syntax highlighting and a hover copy button, plus a standalone CopyButton.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@retab/scroll-area",
    "@retab/utils",
    "@retab/effect-key",
    "@retab/use-keyed-layout-effect",
    "@retab/use-keyed-mount-effect"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/ui/json-inspector.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Check, Copy } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { useKeyedLayoutEffect } from \"@/hooks/use-keyed-layout-effect\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nimport {\n  createJsonInspectorLineHtmlCache,\n  type JsonInspectorLineHtmlCache,\n} from \"./json-inspector-highlight\";\n\nconst SMALL_JSON_LINE_LIMIT = 500;\nconst VIRTUAL_LINE_HEIGHT = 20;\nconst VIRTUAL_OVERSCAN = 8;\nconst INITIAL_VIEWPORT_HEIGHT = 480;\nconst MAX_RENDERED_LINES = 500;\nconst VIRTUAL_JSON_LINE_STYLE: React.CSSProperties = {\n  height: VIRTUAL_LINE_HEIGHT,\n};\n\n/**\n * A small copy-to-clipboard button. Shows a transient check on success; style\n * placement via `className` (e.g. absolute-position it over a panel).\n */\nexport function CopyButton({\n  text,\n  className,\n}: {\n  text: string;\n  className?: string;\n}) {\n  const [copied, setCopied] = React.useState(false);\n\n  return (\n    <button\n      type=\"button\"\n      onClick={() => {\n        void navigator.clipboard.writeText(text).then(() => {\n          setCopied(true);\n          window.setTimeout(() => setCopied(false), 1500);\n        });\n      }}\n      className={cn(\n        \"text-muted-foreground hover:bg-muted hover:text-foreground rounded-md p-1.5 transition-colors\",\n        className,\n      )}\n      title=\"Copy\"\n    >\n      {copied ? (\n        <Check className=\"size-3.5 text-emerald-600 dark:text-emerald-400\" />\n      ) : (\n        <Copy className=\"size-3.5\" />\n      )}\n    </button>\n  );\n}\n\nfunction useJsonLineHtml() {\n  const cacheRef = React.useRef<JsonInspectorLineHtmlCache | null>(null);\n  if (!cacheRef.current) {\n    cacheRef.current = createJsonInspectorLineHtmlCache();\n  }\n  const cache = cacheRef.current;\n\n  return React.useCallback((line: string) => cache.get(line), [cache]);\n}\n\nfunction jsonLineWindow({\n  lineCount,\n  scrollTop,\n  viewportHeight,\n}: {\n  lineCount: number;\n  scrollTop: number;\n  viewportHeight: number;\n}) {\n  if (lineCount <= 0) return { start: 0, end: 0 };\n\n  const safeScrollTop =\n    Number.isFinite(scrollTop) && scrollTop > 0 ? scrollTop : 0;\n  const safeViewportHeight =\n    Number.isFinite(viewportHeight) && viewportHeight > 0\n      ? viewportHeight\n      : INITIAL_VIEWPORT_HEIGHT;\n  const visibleStart = clamp(\n    Math.floor(safeScrollTop / VIRTUAL_LINE_HEIGHT),\n    0,\n    lineCount - 1,\n  );\n  const visibleCount = Math.max(\n    1,\n    Math.ceil(safeViewportHeight / VIRTUAL_LINE_HEIGHT),\n  );\n  const uncappedStart = Math.max(0, visibleStart - VIRTUAL_OVERSCAN);\n  const uncappedEnd = Math.min(\n    lineCount,\n    visibleStart + visibleCount + VIRTUAL_OVERSCAN,\n  );\n\n  if (uncappedEnd - uncappedStart <= MAX_RENDERED_LINES) {\n    return { start: uncappedStart, end: uncappedEnd };\n  }\n\n  return {\n    start: visibleStart,\n    end: Math.min(lineCount, visibleStart + MAX_RENDERED_LINES),\n  };\n}\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(max, Math.max(min, value));\n}\n\nconst JsonInspectorHighlightedLine = React.memo(\n  function JsonInspectorHighlightedLine({\n    html,\n    lineIndex,\n    virtual = false,\n  }: {\n    html: string;\n    lineIndex?: number;\n    virtual?: boolean;\n  }) {\n    return (\n      <div\n        data-json-line-index={lineIndex}\n        dangerouslySetInnerHTML={{ __html: html }}\n        style={virtual ? VIRTUAL_JSON_LINE_STYLE : undefined}\n      />\n    );\n  },\n);\n\nfunction JsonInspectorLines({ lines }: { lines: string[] }) {\n  const htmlForLine = useJsonLineHtml();\n\n  return (\n    <pre className=\"p-3 font-mono text-xs leading-5\">\n      {lines.map((line, i) => (\n        <JsonInspectorHighlightedLine key={i} html={htmlForLine(line)} />\n      ))}\n    </pre>\n  );\n}\n\nfunction VirtualJsonInspectorLines({ lines }: { lines: string[] }) {\n  const htmlForLine = useJsonLineHtml();\n  const viewportRef = React.useRef<HTMLDivElement | null>(null);\n  const frameRef = React.useRef(0);\n  const [windowRange, setWindowRange] = React.useState(() =>\n    jsonLineWindow({\n      lineCount: lines.length,\n      scrollTop: 0,\n      viewportHeight: INITIAL_VIEWPORT_HEIGHT,\n    }),\n  );\n\n  const measure = React.useCallback(() => {\n    frameRef.current = 0;\n    const viewport = viewportRef.current;\n    const nextRange = jsonLineWindow({\n      lineCount: lines.length,\n      scrollTop: viewport?.scrollTop ?? 0,\n      viewportHeight: viewport?.clientHeight ?? INITIAL_VIEWPORT_HEIGHT,\n    });\n    setWindowRange((current) =>\n      current.start === nextRange.start && current.end === nextRange.end\n        ? current\n        : nextRange,\n    );\n  }, [lines.length]);\n\n  const scheduleMeasure = React.useCallback(() => {\n    if (frameRef.current) return;\n    let didRun = false;\n    const frame = requestAnimationFrame(() => {\n      didRun = true;\n      measure();\n    });\n    frameRef.current = didRun ? 0 : frame;\n  }, [measure]);\n\n  useKeyedLayoutEffect(joinEffectKey([measure]), () => {\n    measure();\n  });\n\n  useKeyedMountEffect(joinEffectKey([scheduleMeasure]), () => {\n    const viewport = viewportRef.current;\n    if (!viewport) return;\n\n    viewport.addEventListener(\"scroll\", scheduleMeasure, { passive: true });\n    const observer =\n      typeof ResizeObserver !== \"undefined\"\n        ? new ResizeObserver(scheduleMeasure)\n        : null;\n    observer?.observe(viewport);\n\n    return () => {\n      if (frameRef.current) cancelAnimationFrame(frameRef.current);\n      viewport.removeEventListener(\"scroll\", scheduleMeasure);\n      observer?.disconnect();\n    };\n  });\n\n  const visibleLines = lines.slice(windowRange.start, windowRange.end);\n\n  return (\n    <div\n      ref={viewportRef}\n      data-slot=\"json-inspector-virtual-scroll\"\n      className=\"h-full overflow-auto\"\n    >\n      <pre\n        className=\"relative p-0 font-mono text-xs leading-5\"\n        style={{ height: lines.length * VIRTUAL_LINE_HEIGHT }}\n      >\n        <div\n          className=\"absolute right-0 left-0 px-3 py-3\"\n          style={{\n            transform: `translate3d(0, ${\n              windowRange.start * VIRTUAL_LINE_HEIGHT\n            }px, 0)`,\n          }}\n        >\n          {visibleLines.map((line, offset) => {\n            const lineIndex = windowRange.start + offset;\n            return (\n              <JsonInspectorHighlightedLine\n                key={lineIndex}\n                html={htmlForLine(line)}\n                lineIndex={lineIndex}\n                virtual\n              />\n            );\n          })}\n        </div>\n      </pre>\n    </div>\n  );\n}\n\n/**\n * A read-only JSON viewer: pretty-prints `data`, applies theme-aware syntax\n * highlighting, scrolls within its container, and reveals a copy button on hover.\n */\nexport function JsonInspector({\n  data,\n  className,\n}: {\n  data: unknown;\n  className?: string;\n}) {\n  const formatted = React.useMemo(() => JSON.stringify(data, null, 2), [data]);\n  const lines = React.useMemo(() => formatted.split(\"\\n\"), [formatted]);\n  const shouldVirtualize = lines.length > SMALL_JSON_LINE_LIMIT;\n\n  return (\n    <div className={cn(\"group relative h-full\", className)}>\n      {shouldVirtualize ? (\n        <VirtualJsonInspectorLines lines={lines} />\n      ) : (\n        <ScrollArea className=\"h-full\">\n          <JsonInspectorLines lines={lines} />\n        </ScrollArea>\n      )}\n      <CopyButton\n        text={formatted}\n        className=\"absolute top-2 right-2 opacity-0 transition-opacity group-hover:opacity-100\"\n      />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/json-inspector.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/json-inspector-highlight.ts",
      "content": "const JSON_DEFAULT_TEXT_CLASS_NAME = \"text-foreground/70\";\nconst JSON_INSPECTOR_HTML_CACHE_LIMIT = 4_096;\n\ntype JsonInspectorTokenSpan = {\n  className: string;\n  end: number;\n  start: number;\n  text: string;\n};\n\ntype JsonInspectorHighlightPattern = {\n  className: string;\n  regex: RegExp;\n};\n\nexport type JsonInspectorLineHtmlCache = {\n  get(line: string): string;\n  readonly size: number;\n};\n\nconst JSON_HIGHLIGHT_PATTERNS: readonly JsonInspectorHighlightPattern[] = [\n  {\n    regex: /\"([^\"]+)\"(?=\\s*:)/g,\n    className: \"text-violet-600 dark:text-violet-400\",\n  },\n  { regex: /\"([^\"]*)\"/g, className: \"text-amber-700 dark:text-amber-400\" },\n  {\n    regex: /\\b(true|false)\\b/g,\n    className: \"text-emerald-600 dark:text-emerald-400\",\n  },\n  { regex: /\\bnull\\b/g, className: \"text-muted-foreground\" },\n  {\n    regex: /\\b(\\d+\\.?\\d*)\\b/g,\n    className: \"text-blue-600 dark:text-blue-400\",\n  },\n];\n\nexport function createJsonInspectorLineHtmlCache(\n  maxSize = JSON_INSPECTOR_HTML_CACHE_LIMIT,\n): JsonInspectorLineHtmlCache {\n  const cache = new Map<string, string>();\n  const limit = Math.max(1, Math.floor(maxSize));\n\n  return {\n    get(line) {\n      const cached = cache.get(line);\n      if (cached !== undefined) {\n        cache.delete(line);\n        cache.set(line, cached);\n        return cached;\n      }\n\n      const html = jsonInspectorLineToHtml(line);\n      cache.set(line, html);\n      while (cache.size > limit) {\n        const oldest = cache.keys().next().value as string | undefined;\n        if (oldest === undefined) break;\n        cache.delete(oldest);\n      }\n      return html;\n    },\n    get size() {\n      return cache.size;\n    },\n  };\n}\n\nexport function jsonInspectorLineToHtml(line: string): string {\n  const spans = jsonInspectorTokenSpans(line);\n  if (spans.length === 0) {\n    return spanToHtml(JSON_DEFAULT_TEXT_CLASS_NAME, line);\n  }\n\n  spans.sort((a, b) => a.start - b.start);\n  let html = \"\";\n  let lastEnd = 0;\n\n  for (const span of spans) {\n    if (span.start > lastEnd) {\n      html += spanToHtml(\n        JSON_DEFAULT_TEXT_CLASS_NAME,\n        line.slice(lastEnd, span.start),\n      );\n    }\n    html += spanToHtml(span.className, span.text);\n    lastEnd = span.end;\n  }\n\n  if (lastEnd < line.length) {\n    html += spanToHtml(JSON_DEFAULT_TEXT_CLASS_NAME, line.slice(lastEnd));\n  }\n\n  return html;\n}\n\nfunction jsonInspectorTokenSpans(line: string): JsonInspectorTokenSpan[] {\n  const spans: JsonInspectorTokenSpan[] = [];\n\n  for (const { regex, className } of JSON_HIGHLIGHT_PATTERNS) {\n    const re = new RegExp(regex.source, \"g\");\n    let match: RegExpExecArray | null;\n    while ((match = re.exec(line)) !== null) {\n      const start = match.index;\n      const end = start + match[0].length;\n      const overlaps = spans.some(\n        (span) => start < span.end && end > span.start,\n      );\n      if (!overlaps) {\n        spans.push({ start, end, className, text: match[0] });\n      }\n    }\n  }\n\n  return spans;\n}\n\nfunction spanToHtml(className: string, text: string) {\n  return `<span class=\"${className}\">${escapeJsonInspectorHtml(text)}</span>`;\n}\n\nfunction escapeJsonInspectorHtml(value: string) {\n  return value\n    .replace(/&/g, \"&amp;\")\n    .replace(/</g, \"&lt;\")\n    .replace(/>/g, \"&gt;\");\n}\n",
      "type": "registry:ui",
      "target": "@ui/json-inspector-highlight.ts"
    }
  ],
  "type": "registry:ui"
}