{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "json-view",
  "title": "JSON Viewer (Base UI)",
  "description": "A fully-customizable, collapsible, syntax-highlighted JSON tree viewer component. For projects using Base UI / base-mira style.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "tooltip"
  ],
  "files": [
    {
      "path": "registry/json-view/json-view.tsx",
      "content": "\"use client\"\n\nimport {\n  Check,\n  ChevronRight,\n  ChevronUp,\n  Copy,\n  MoreHorizontal,\n} from \"lucide-react\"\nimport { useCallback, useState } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { cn } from \"@/lib/utils\"\n\ntype JsonPrimitive = string | number | boolean | null\ntype JsonValueType = JsonPrimitive | JsonObjectType | JsonArrayType\ntype JsonObjectType = { [key: string]: JsonValueType }\ntype JsonArrayType = JsonValueType[]\n\ninterface JsonViewTheme {\n  key?: string\n  string?: string\n  number?: string\n  boolean?: string\n  null?: string\n  bracket?: string\n  lineHover?: string\n}\n\nconst defaultTheme: Required<JsonViewTheme> = {\n  key: \"text-blue-700 dark:text-blue-400\",\n  string: \"text-green-700 dark:text-green-400\",\n  number: \"text-orange-700 dark:text-amber-400\",\n  boolean: \"text-purple-700 dark:text-purple-400\",\n  null: \"text-gray-500 dark:text-gray-400 italic\",\n  bracket: \"text-gray-700 dark:text-gray-300\",\n  lineHover: \"hover:bg-muted/50\",\n}\n\ninterface JsonViewProps {\n  data: unknown\n  className?: string\n  defaultExpanded?: boolean\n  initialDepth?: number\n  indentGuide?: boolean\n  theme?: JsonViewTheme\n  rootName?: string\n  stringTruncate?: number\n}\n\ninterface InternalProps {\n  indentGuide: boolean\n  theme: Required<JsonViewTheme>\n  initialDepth: number\n  stringTruncate: number\n}\n\nfunction isObject(value: unknown): value is JsonObjectType {\n  return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\nfunction isArray(value: unknown): value is JsonArrayType {\n  return Array.isArray(value)\n}\n\n// Resolve values that are not plain JSON into the closest JSON-like shape,\n// mirroring what JSON.stringify would do where it is defined: objects with\n// toJSON (Date, custom classes) use it, Map becomes an object, Set an array.\nfunction resolveValue(value: unknown): unknown {\n  if (value === null || typeof value !== \"object\") return value\n  if (typeof (value as { toJSON?: unknown }).toJSON === \"function\") {\n    return (value as { toJSON: () => unknown }).toJSON()\n  }\n  if (value instanceof Map) {\n    return Object.fromEntries(Array.from(value, ([k, v]) => [String(k), v]))\n  }\n  if (value instanceof Set) return Array.from(value)\n  return value\n}\n\nfunction stringifyValue(value: unknown): string {\n  if (typeof value === \"string\") return value\n  // Track the ancestor chain so circular references become \"[Circular]\"\n  // instead of throwing. `this` inside the replacer is the parent object.\n  const ancestors: unknown[] = []\n  return JSON.stringify(\n    value,\n    function (this: unknown, _key, val) {\n      while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {\n        ancestors.pop()\n      }\n      const resolved = resolveValue(val)\n      if (typeof resolved === \"bigint\") {\n        const asNumber = Number(resolved)\n        return Number.isSafeInteger(asNumber) ? asNumber : resolved.toString()\n      }\n      if (resolved !== null && typeof resolved === \"object\") {\n        if (ancestors.includes(resolved)) return \"[Circular]\"\n        ancestors.push(resolved)\n      }\n      return resolved\n    },\n    2\n  )\n}\n\nfunction CopyButton({ value }: { value: unknown }) {\n  const [copied, setCopied] = useState(false)\n\n  const handleCopy = useCallback(() => {\n    const text = stringifyValue(value)\n    navigator.clipboard.writeText(text).then(() => {\n      setCopied(true)\n      setTimeout(() => setCopied(false), 1500)\n    })\n  }, [value])\n\n  return (\n    <div className=\"inline-flex shrink-0\" onClick={(e) => e.stopPropagation()}>\n      <Tooltip>\n        <TooltipTrigger\n          render={\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              aria-label={copied ? \"Copied\" : \"Copy value\"}\n              className=\"opacity-0 transition-opacity group-hover/line:opacity-100 group-has-[:focus-visible]/line:opacity-100 focus-visible:opacity-100\"\n              onClick={handleCopy}\n            />\n          }\n        >\n          {copied ? (\n            <Check className=\"size-3 text-green-600 dark:text-green-400\" />\n          ) : (\n            <Copy className=\"size-3 text-muted-foreground\" />\n          )}\n        </TooltipTrigger>\n        <TooltipContent side=\"top\" sideOffset={4}>\n          {copied ? \"Copied!\" : \"Copy\"}\n        </TooltipContent>\n      </Tooltip>\n    </div>\n  )\n}\n\nfunction JsonLine({\n  depth,\n  value,\n  children,\n  className,\n  theme,\n}: {\n  depth: number\n  value: unknown\n  children: React.ReactNode\n  className?: string\n  theme: Required<JsonViewTheme>\n}) {\n  return (\n    <div\n      className={cn(\n        \"group/line flex items-center gap-1 rounded-sm rounded-l-none leading-6\",\n        theme.lineHover,\n        className\n      )}\n      style={{ paddingLeft: depth * 24 }}\n    >\n      <span className=\"min-w-0 [overflow-wrap:anywhere]\">{children}</span>\n      <CopyButton value={value} />\n    </div>\n  )\n}\n\nfunction Bracket({\n  children,\n  theme,\n}: {\n  children: React.ReactNode\n  theme: Required<JsonViewTheme>\n}) {\n  return <span className={cn(\"font-semibold\", theme.bracket)}>{children}</span>\n}\n\nfunction Comma({ theme }: { theme: Required<JsonViewTheme> }) {\n  return <span className={theme.bracket}>,</span>\n}\n\n// Escape a string the way JSON.stringify would, without the surrounding quotes,\n// so embedded quotes, backslashes, and control characters display faithfully.\nfunction escapeString(value: string): string {\n  return JSON.stringify(value).slice(1, -1)\n}\n\nfunction KeyLabel({\n  name,\n  theme,\n}: {\n  name: string\n  theme: Required<JsonViewTheme>\n}) {\n  return (\n    <span className={cn(\"whitespace-pre-wrap\", theme.key)}>\n      &quot;{escapeString(name)}&quot;\n      <span className={theme.bracket}>: </span>\n    </span>\n  )\n}\n\nfunction StringValue({\n  value,\n  theme,\n}: {\n  value: string\n  theme: Required<JsonViewTheme>\n}) {\n  return (\n    <span className={cn(\"whitespace-pre-wrap\", theme.string)}>\n      &quot;{escapeString(value)}&quot;\n    </span>\n  )\n}\n\nfunction TruncatedString({\n  value,\n  truncate,\n  theme,\n}: {\n  value: string\n  truncate: number\n  theme: Required<JsonViewTheme>\n}) {\n  const [expanded, setExpanded] = useState(false)\n\n  if (value.length <= truncate) {\n    return <StringValue value={value} theme={theme} />\n  }\n\n  return (\n    <span\n      className={cn(\n        \"group/truncated cursor-pointer whitespace-pre-wrap\",\n        theme.string\n      )}\n      onClick={(e) => {\n        e.stopPropagation()\n        setExpanded((prev) => !prev)\n      }}\n    >\n      &quot;\n      {expanded ? (\n        escapeString(value)\n      ) : (\n        <Tooltip>\n          <TooltipTrigger render={<span />}>\n            {escapeString(value.substring(0, truncate))}&hellip;\n          </TooltipTrigger>\n          <TooltipContent\n            side=\"bottom\"\n            sideOffset={4}\n            className=\"max-w-xs wrap-break-word whitespace-pre-wrap\"\n          >\n            {value}\n          </TooltipContent>\n        </Tooltip>\n      )}\n      &quot;\n      <button\n        type=\"button\"\n        aria-expanded={expanded}\n        aria-label={expanded ? \"Collapse string\" : \"Expand string\"}\n        className=\"ml-1 inline-flex cursor-pointer rounded-sm align-middle opacity-0 transition-opacity outline-none group-hover/truncated:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring/50\"\n        onClick={(e) => {\n          e.stopPropagation()\n          setExpanded((prev) => !prev)\n        }}\n      >\n        {expanded ? (\n          <ChevronUp className=\"size-3 text-muted-foreground\" />\n        ) : (\n          <MoreHorizontal className=\"size-3 text-muted-foreground\" />\n        )}\n      </button>\n    </span>\n  )\n}\n\nfunction JsonPrimitiveValue({\n  value,\n  theme,\n  stringTruncate,\n}: {\n  value: unknown\n  theme: Required<JsonViewTheme>\n  stringTruncate: number\n}) {\n  if (value === null) {\n    return <span className={theme.null}>null</span>\n  }\n\n  if (value === undefined) {\n    return <span className={theme.null}>undefined</span>\n  }\n\n  if (typeof value === \"boolean\") {\n    return <span className={theme.boolean}>{String(value)}</span>\n  }\n\n  if (typeof value === \"number\") {\n    return <span className={theme.number}>{String(value)}</span>\n  }\n\n  if (typeof value === \"string\") {\n    if (stringTruncate > 0) {\n      return (\n        <TruncatedString\n          value={value}\n          truncate={stringTruncate}\n          theme={theme}\n        />\n      )\n    }\n\n    return <StringValue value={value} theme={theme} />\n  }\n\n  if (typeof value === \"bigint\") {\n    return <span className={theme.number}>{value.toString()}</span>\n  }\n\n  if (typeof value === \"function\") {\n    return (\n      <span className={theme.null}>\n        [Function{value.name ? `: ${value.name}` : \"\"}]\n      </span>\n    )\n  }\n\n  // Remaining non-JSON primitives (symbol): render without quotes so they\n  // are not mistaken for strings.\n  return <span className={theme.null}>{String(value)}</span>\n}\n\nfunction CollapsibleNode({\n  value,\n  keyName,\n  depth,\n  absoluteDepth,\n  isLast,\n  internal,\n  ancestors,\n}: {\n  value: JsonObjectType | JsonArrayType\n  keyName?: string\n  depth: number\n  absoluteDepth: number\n  isLast: boolean\n  internal: InternalProps\n  ancestors: readonly object[]\n}) {\n  const shouldExpand =\n    internal.initialDepth === Infinity\n      ? true\n      : absoluteDepth < internal.initialDepth\n\n  const [expanded, setExpanded] = useState(shouldExpand)\n\n  const { theme, indentGuide } = internal\n  const isArr = isArray(value)\n  const entries = isArr\n    ? value.map((v, i) => [i, v] as const)\n    : Object.entries(value)\n  const openBracket = isArr ? \"[\" : \"{\"\n  const closeBracket = isArr ? \"]\" : \"}\"\n  const isEmpty = entries.length === 0\n  const childAncestors = [...ancestors, value]\n\n  // Empty containers render inline\n  if (isEmpty) {\n    return (\n      <JsonLine depth={depth} value={value} theme={theme}>\n        {keyName !== undefined && <KeyLabel name={keyName} theme={theme} />}\n        <Bracket theme={theme}>\n          {openBracket}\n          {closeBracket}\n        </Bracket>\n        {!isLast && <Comma theme={theme} />}\n      </JsonLine>\n    )\n  }\n\n  return (\n    <div>\n      {/* Opening line */}\n      <div\n        className={cn(\n          \"group/line flex items-center gap-1 rounded-sm rounded-l-none leading-6\",\n          theme.lineHover\n        )}\n        style={{ paddingLeft: depth * 24 }}\n      >\n        <button\n          type=\"button\"\n          aria-expanded={expanded}\n          className=\"flex min-w-0 flex-1 cursor-pointer items-center rounded-sm text-left outline-none focus-visible:ring-2 focus-visible:ring-ring/50\"\n          onClick={() => setExpanded((e) => !e)}\n        >\n          <span className=\"mr-1 inline-flex shrink-0 items-center justify-center p-0.5 text-muted-foreground\">\n            <ChevronRight\n              className={cn(\n                \"size-3.5 transition-transform duration-150\",\n                expanded && \"rotate-90\"\n              )}\n            />\n          </span>\n          <span className=\"min-w-0 [overflow-wrap:anywhere]\">\n            {keyName !== undefined && <KeyLabel name={keyName} theme={theme} />}\n            <Bracket theme={theme}>{openBracket}</Bracket>\n            {!expanded && (\n              <span className=\"ml-1 text-xs text-muted-foreground\">\n                {isArr\n                  ? `\\u2026${entries.length} items`\n                  : `\\u2026${entries.length} keys`}\n              </span>\n            )}\n            {!expanded && (\n              <>\n                <Bracket theme={theme}>{closeBracket}</Bracket>\n                {!isLast && <Comma theme={theme} />}\n              </>\n            )}\n          </span>\n        </button>\n        <CopyButton value={value} />\n      </div>\n\n      {/* Children with indent guide */}\n      {expanded && (\n        <div\n          className={cn(\n            indentGuide &&\n              \"border-l border-muted-foreground/20 transition-colors hover:border-muted-foreground/40\"\n          )}\n          style={{ marginLeft: depth * 24 + 10 }}\n        >\n          {entries.map(([key, childValue], idx) => (\n            <JsonNode\n              key={String(key)}\n              value={childValue}\n              keyName={isArr ? undefined : String(key)}\n              depth={1}\n              absoluteDepth={absoluteDepth + 1}\n              isLast={idx === entries.length - 1}\n              internal={internal}\n              ancestors={childAncestors}\n            />\n          ))}\n        </div>\n      )}\n\n      {/* Closing bracket */}\n      {expanded && (\n        <div className=\"leading-6\" style={{ paddingLeft: depth * 24 }}>\n          <span className=\"ml-5\">\n            <Bracket theme={theme}>{closeBracket}</Bracket>\n            {!isLast && <Comma theme={theme} />}\n          </span>\n        </div>\n      )}\n    </div>\n  )\n}\n\nfunction JsonNode({\n  value: rawValue,\n  keyName,\n  depth,\n  absoluteDepth,\n  isLast,\n  internal,\n  ancestors,\n}: {\n  value: unknown\n  keyName?: string\n  depth: number\n  absoluteDepth: number\n  isLast: boolean\n  internal: InternalProps\n  ancestors: readonly object[]\n}) {\n  const value = resolveValue(rawValue)\n\n  if (isObject(value) || isArray(value)) {\n    if (ancestors.includes(value)) {\n      return (\n        <JsonLine depth={depth} value={value} theme={internal.theme}>\n          {keyName !== undefined && (\n            <KeyLabel name={keyName} theme={internal.theme} />\n          )}\n          <span className={internal.theme.null}>[Circular]</span>\n          {!isLast && <Comma theme={internal.theme} />}\n        </JsonLine>\n      )\n    }\n\n    return (\n      <CollapsibleNode\n        value={value}\n        keyName={keyName}\n        depth={depth}\n        absoluteDepth={absoluteDepth}\n        isLast={isLast}\n        internal={internal}\n        ancestors={ancestors}\n      />\n    )\n  }\n\n  return (\n    <JsonLine depth={depth} value={value} theme={internal.theme}>\n      {keyName !== undefined && (\n        <KeyLabel name={keyName} theme={internal.theme} />\n      )}\n      <JsonPrimitiveValue\n        value={value}\n        theme={internal.theme}\n        stringTruncate={internal.stringTruncate}\n      />\n      {!isLast && <Comma theme={internal.theme} />}\n    </JsonLine>\n  )\n}\n\nfunction JsonView({\n  data,\n  className,\n  defaultExpanded = true,\n  initialDepth,\n  indentGuide = true,\n  theme: themeOverride,\n  rootName,\n  stringTruncate = 0,\n}: JsonViewProps) {\n  const theme: Required<JsonViewTheme> = { ...defaultTheme, ...themeOverride }\n\n  const resolvedInitialDepth =\n    initialDepth !== undefined ? initialDepth : defaultExpanded ? Infinity : 0\n\n  const internal: InternalProps = {\n    indentGuide,\n    theme,\n    initialDepth: resolvedInitialDepth,\n    stringTruncate,\n  }\n\n  const content = (\n    <JsonNode\n      value={data}\n      keyName={rootName}\n      depth={0}\n      absoluteDepth={0}\n      isLast\n      internal={internal}\n      ancestors={[]}\n    />\n  )\n\n  return (\n    <TooltipProvider>\n      <div className={cn(\"font-mono text-sm\", className)}>{content}</div>\n    </TooltipProvider>\n  )\n}\n\nexport { JsonView }\nexport type { JsonViewProps, JsonViewTheme }\n",
      "type": "registry:ui",
      "target": "@ui/json-view.tsx"
    }
  ],
  "type": "registry:ui"
}