{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "json-viewer", "title": "JSON Viewer", "description": "Expandable structured JSON viewer with accessible controls and copy actions.", "registryDependencies": [ "askyourpolicy/ss-registry/stitch-base", "askyourpolicy/ss-registry/button", "askyourpolicy/ss-registry/tooltip" ], "files": [ { "path": "src/components/ui/json-viewer.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n CaretDownIcon,\n CaretRightIcon,\n CaretUpIcon,\n CheckIcon,\n CopyIcon,\n DotsThreeIcon,\n} from \"@phosphor-icons/react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\nexport type JsonViewerProps = {\n className?: string;\n data?: unknown;\n defaultExpanded?: boolean;\n maxHeight?: string;\n rootName?: string;\n value?: unknown;\n};\n\nexport function JsonViewer({\n className,\n data,\n defaultExpanded = false,\n maxHeight = \"max-h-72\",\n rootName = \"root\",\n value,\n}: JsonViewerProps) {\n const source = value !== undefined ? value : data;\n\n return (\n \n \n \n \n \n \n \n \n \n );\n}\n\ntype JsonNodeProps = {\n data: unknown;\n defaultExpanded: boolean;\n isRoot?: boolean;\n level: number;\n name: string;\n};\n\nfunction JsonNode({ data, defaultExpanded, isRoot = false, level, name }: JsonNodeProps) {\n const [expanded, setExpanded] = React.useState(defaultExpanded);\n const [copyState, setCopyState] = React.useState<\"idle\" | \"copied\" | \"error\">(\"idle\");\n const copyStateTimer = React.useRef(null);\n const contentId = React.useId();\n const dataType = getJsonType(data);\n const expandable = dataType === \"object\" || dataType === \"array\";\n const entries = expandable ? getEntries(data) : [];\n\n React.useEffect(\n () => () => {\n if (copyStateTimer.current !== null) {\n window.clearTimeout(copyStateTimer.current);\n }\n },\n [],\n );\n\n async function copyToClipboard(event: React.MouseEvent) {\n event.stopPropagation();\n\n if (copyStateTimer.current !== null) {\n window.clearTimeout(copyStateTimer.current);\n }\n\n try {\n const serialized = JSON.stringify(data, null, 2) ?? String(data);\n if (!navigator.clipboard?.writeText) {\n throw new Error(\"Clipboard access is unavailable.\");\n }\n await navigator.clipboard.writeText(serialized);\n setCopyState(\"copied\");\n } catch {\n setCopyState(\"error\");\n }\n\n copyStateTimer.current = window.setTimeout(() => {\n setCopyState(\"idle\");\n copyStateTimer.current = null;\n }, 1600);\n }\n\n const rowContent = (\n <>\n \n {expandable ? (\n expanded ? (\n \n ) : (\n \n )\n ) : null}\n \n {name}\n \n {expandable ? (\n <>\n {dataType === \"array\" ? \"[\" : \"{\"}\n {!expanded ? (\n <>\n {\" \"}\n {entries.length} {entries.length === 1 ? \"item\" : \"items\"}{\" \"}\n {dataType === \"array\" ? \"]\" : \"}\"}\n >\n ) : null}\n >\n ) : (\n \":\"\n )}\n \n {!expandable ? : null}\n >\n );\n\n return (\n 0 && \"border-l border-border-subtle\")}>\n \n {expandable ? (\n setExpanded((current) => !current)}\n type=\"button\"\n >\n {rowContent}\n \n ) : (\n \n {rowContent}\n \n )}\n void copyToClipboard(event)}\n size=\"icon-xs\"\n type=\"button\"\n variant=\"ghost\"\n >\n {copyState === \"copied\" ? (\n \n ) : (\n \n )}\n \n \n {copyState === \"copied\"\n ? `${name} copied to clipboard.`\n : copyState === \"error\"\n ? `${name} could not be copied. Try again.`\n : \"\"}\n \n \n {expandable && expanded ? (\n \n {entries.map(([key, entry]) => (\n \n ))}\n \n {dataType === \"array\" ? \"]\" : \"}\"}\n \n \n ) : null}\n \n );\n}\n\nfunction JsonValue({ data }: { data: unknown }) {\n const [expanded, setExpanded] = React.useState(false);\n const textLimit = 80;\n\n if (data === null) {\n return null;\n }\n if (data === undefined) {\n return undefined;\n }\n if (data instanceof Date) {\n return {data.toISOString()};\n }\n if (typeof data === \"string\") {\n if (data.length > textLimit) {\n const displayValue = expanded ? data : `${data.slice(0, textLimit)}...`;\n return (\n {\n event.stopPropagation();\n setExpanded((current) => !current);\n }}\n type=\"button\"\n >\n \"\n {expanded ? (\n {displayValue}\n ) : (\n \n }>{displayValue}\n \n {data}\n \n \n )}\n \"\n {expanded ? (\n \n ) : (\n \n )}\n \n );\n }\n return {`\"${data}\"`};\n }\n if (typeof data === \"number\") {\n return {data};\n }\n if (typeof data === \"boolean\") {\n return {String(data)};\n }\n return {String(data)};\n}\n\nfunction getJsonType(value: unknown) {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"array\";\n if (value instanceof Date) return \"date\";\n return typeof value;\n}\n\nfunction getEntries(value: unknown): [string, unknown][] {\n if (Array.isArray(value)) return value.map((entry, index) => [String(index), entry]);\n if (value && typeof value === \"object\") return Object.entries(value);\n return [];\n}\n", "type": "registry:ui" } ], "type": "registry:ui" }