{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "json-viewer",
  "title": "JSON Viewer",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A collapsible JSON inspector with compact type styling and keyboard-operable branches.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/json-viewer.tsx",
      "content": "// Ballmac UI: JSON Viewer. https://ui.ballmac.com/components/json-viewer\n\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronRight } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype JsonViewerProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** JSON-compatible value to inspect. */\n  value: unknown\n  /** Accessible label for the data. */\n  label?: string\n  /** Number of levels open initially. */\n  defaultExpandedDepth?: number\n}\n\nfunction JsonViewer({\n  className,\n  value,\n  label,\n  defaultExpandedDepth = 1,\n  ...props\n}: JsonViewerProps) {\n  const msg = useMessages()\n  label ??= msg(\"json-viewer.label\", \"JSON data\")\n  const [overrides, setOverrides] = React.useState<Record<string, boolean>>({})\n  function renderNode(\n    name: string | undefined,\n    item: unknown,\n    path: string,\n    depth: number,\n  ): React.ReactNode {\n    const nested = item !== null && typeof item === \"object\"\n    const entries = nested\n      ? Object.entries(item as Record<string, unknown>)\n      : []\n    const expanded = overrides[path] ?? depth < defaultExpandedDepth\n    const summary = Array.isArray(item)\n      ? `[${entries.length}]`\n      : `{${entries.length}}`\n    return (\n      <div key={path} data-slot=\"json-viewer-node\" className=\"min-w-0\">\n        <div\n          className=\"flex min-w-0 items-start gap-1 leading-6\"\n          style={{ paddingInlineStart: `${depth * 16}px` }}\n        >\n          {nested ? (\n            <button\n              type=\"button\"\n              data-slot=\"json-viewer-toggle\"\n              aria-label={expanded ? msg(\"json-viewer.collapse\", \"Collapse {name}\", { name: name ?? msg(\"json-viewer.root\", \"root\") }) : msg(\"json-viewer.expand\", \"Expand {name}\", { name: name ?? msg(\"json-viewer.root\", \"root\") })}\n              aria-expanded={expanded}\n              onClick={() =>\n                setOverrides((current) => ({ ...current, [path]: !expanded }))\n              }\n              className=\"hover:bg-accent mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n            >\n              <ChevronRight\n                aria-hidden=\"true\"\n                className={cn(\n                  \"size-3.5 transition-transform duration-150 motion-reduce:transition-none\",\n                  expanded ? \"rotate-90\" : \"rtl:rotate-180\",\n                )}\n              />\n            </button>\n          ) : (\n            <span aria-hidden=\"true\" className=\"size-5 shrink-0\" />\n          )}\n          <span className=\"min-w-0 break-all\">\n            {name !== undefined && (\n              <span className=\"text-muted-foreground\">\n                {JSON.stringify(name)}:{\" \"}\n              </span>\n            )}\n            {nested ? (\n              <span className=\"text-muted-foreground\">{summary}</span>\n            ) : (\n              <span\n                className={cn(\n                  typeof item === \"string\"\n                    ? \"text-foreground\"\n                    : typeof item === \"number\" || typeof item === \"boolean\"\n                      ? \"text-primary\"\n                      : \"text-muted-foreground\",\n                )}\n              >\n                {JSON.stringify(item) ?? \"undefined\"}\n              </span>\n            )}\n          </span>\n        </div>\n        {nested && expanded && (\n          <div data-slot=\"json-viewer-children\">\n            {entries.map(([key, child]) =>\n              renderNode(\n                key,\n                child,\n                `${path}.${JSON.stringify(key)}`,\n                depth + 1,\n              ),\n            )}\n          </div>\n        )}\n      </div>\n    )\n  }\n  return (\n    <div\n      data-slot=\"json-viewer\"\n      dir=\"ltr\"\n      role=\"region\"\n      aria-label={label}\n      tabIndex={0}\n      className={cn(\n        \"bg-card max-h-72 min-w-0 max-w-full overflow-auto rounded-xl border border-border p-3 font-mono text-xs outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n        className,\n      )}\n      {...props}\n    >\n      {renderNode(undefined, value, \"$\", 0)}\n    </div>\n  )\n}\nexport { JsonViewer, type JsonViewerProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/json-viewer.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "json",
      "data",
      "developer"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "A collapsible JSON inspector with compact type styling and keyboard-operable branches.",
      "whenToUse": [
        "Inspect nested API responses or configuration"
      ],
      "whenNotToUse": [
        "Use Code Block for unstructured text"
      ],
      "composesWith": [
        "code-block"
      ],
      "a11y": [
        {
          "keys": "Tab / Enter / Space",
          "action": "Focuses and toggles JSON branches"
        }
      ],
      "customization": [
        "Controlled and uncontrolled state",
        "Content and token-based className styling"
      ]
    },
    "examples": [
      "json-viewer-demo",
      "json-viewer-states"
    ],
    "url": "https://ui.ballmac.com/components/json-viewer"
  },
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}