{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/json",
  "title": "JSON",
  "description": "Pretty-printed JSON with collapsible nodes",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/interaction.json",
    "https://termcn.dev/r/ink/use-theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/json.tsx",
      "content": "import { Box, Text } from \"ink\";\nimport React, { useEffect, useState } from \"react\";\n\nimport { useInteraction } from \"@/hooks/use-interaction\";\nimport type { InteractionProps } from \"@/hooks/use-interaction\";\nimport { useTheme } from \"@/hooks/use-theme\";\n\nexport interface JSONViewProps extends InteractionProps {\n  data: unknown;\n  indent?: number;\n  collapsed?: boolean;\n  label?: string;\n  \"aria-label\"?: string;\n}\n\ninterface JSONLine {\n  path: string;\n  depth: number;\n  key?: string;\n  value?: unknown;\n  type: \"open\" | \"close\" | \"primitive\";\n  openChar?: string;\n  closeChar?: string;\n  isLast: boolean;\n  collapsible: boolean;\n}\n\nconst buildLines = (\n  data: unknown,\n  depth: number,\n  key: string | undefined,\n  isLast: boolean,\n  pathPrefix: string\n): JSONLine[] => {\n  const path = key === undefined ? pathPrefix : `${pathPrefix}.${key}`;\n\n  if (Array.isArray(data)) {\n    const lines: JSONLine[] = [\n      {\n        closeChar: \"]\",\n        collapsible: true,\n        depth,\n        isLast,\n        key,\n        openChar: \"[\",\n        path,\n        type: \"open\",\n      },\n    ];\n    for (const [idx, item] of data.entries()) {\n      const childLines = buildLines(\n        item,\n        depth + 1,\n        undefined,\n        idx === data.length - 1,\n        path\n      );\n      lines.push(...childLines);\n    }\n    lines.push({\n      closeChar: \"]\",\n      collapsible: false,\n      depth,\n      isLast,\n      path: `${path}_close`,\n      type: \"close\",\n    });\n    return lines;\n  }\n\n  if (data !== null && typeof data === \"object\") {\n    const entries = Object.entries(data as Record<string, unknown>);\n    const lines: JSONLine[] = [\n      {\n        closeChar: \"}\",\n        collapsible: true,\n        depth,\n        isLast,\n        key,\n        openChar: \"{\",\n        path,\n        type: \"open\",\n      },\n    ];\n    for (const [idx, [k, v]] of entries.entries()) {\n      const childLines = buildLines(\n        v,\n        depth + 1,\n        k,\n        idx === entries.length - 1,\n        path\n      );\n      lines.push(...childLines);\n    }\n    lines.push({\n      closeChar: \"}\",\n      collapsible: false,\n      depth,\n      isLast,\n      path: `${path}_close`,\n      type: \"close\",\n    });\n    return lines;\n  }\n\n  return [\n    {\n      collapsible: false,\n      depth,\n      isLast,\n      key,\n      path,\n      type: \"primitive\",\n      value: data,\n    },\n  ];\n};\n\nexport const JSONView = ({\n  data,\n  indent = 2,\n  collapsed = false,\n  label,\n  id,\n  autoFocus,\n  isActive,\n  disabled,\n  \"aria-label\": ariaLabel = label ?? \"JSON value\",\n}: JSONViewProps) => {\n  const theme = useTheme();\n  const [collapsedPaths, setCollapsedPaths] = useState<Set<string>>(new Set());\n  const [cursor, setCursor] = useState(0);\n\n  const lines = buildLines(data, 0, undefined, true, \"root\");\n\n  const visibleLines: JSONLine[] = [];\n  const collapsedOpenPaths = new Set<string>();\n\n  for (const line of lines) {\n    const isHidden = [...collapsedOpenPaths].some(\n      (cp) =>\n        line.path.startsWith(`${cp}.`) ||\n        (line.path.startsWith(`${cp}_close`) && line.path !== `${cp}_close`)\n    );\n\n    if (line.type === \"close\") {\n      const openPath = line.path.replace(\"_close\", \"\");\n      if (collapsedOpenPaths.has(openPath)) {\n        continue;\n      }\n    }\n\n    if (!isHidden) {\n      visibleLines.push(line);\n    }\n\n    if (\n      line.type === \"open\" &&\n      (collapsedPaths.has(line.path) ||\n        (collapsed && collapsedPaths.size === 0 && line.depth === 0))\n    ) {\n      collapsedOpenPaths.add(line.path);\n    }\n  }\n\n  const { isFocused } = useInteraction(\n    (input, key) => {\n      if (key.upArrow) {\n        setCursor((c) => Math.max(0, c - 1));\n      } else if (key.downArrow) {\n        setCursor((c) => Math.min(visibleLines.length - 1, c + 1));\n      } else if (key.home) {\n        setCursor(0);\n      } else if (key.end) {\n        setCursor(Math.max(0, visibleLines.length - 1));\n      } else if (input === \" \") {\n        const line = visibleLines[cursor];\n        if (line && line.collapsible) {\n          setCollapsedPaths((prev) => {\n            const next = new Set(prev);\n            if (next.has(line.path)) {\n              next.delete(line.path);\n            } else {\n              next.add(line.path);\n            }\n            return next;\n          });\n        }\n      }\n    },\n    { autoFocus, disabled, id, isActive }\n  );\n\n  useEffect(() => {\n    setCursor((current) =>\n      Math.min(current, Math.max(0, visibleLines.length - 1))\n    );\n  }, [visibleLines.length]);\n\n  const renderValue = (value: unknown): React.ReactNode => {\n    if (value === null) {\n      return <Text color={theme.colors.mutedForeground}>null</Text>;\n    }\n    if (typeof value === \"string\") {\n      return <Text color={theme.colors.success}>&quot;{value}&quot;</Text>;\n    }\n    if (typeof value === \"number\" || typeof value === \"boolean\") {\n      return <Text color={theme.colors.warning}>{String(value)}</Text>;\n    }\n    return <Text color={theme.colors.foreground}>{String(value)}</Text>;\n  };\n\n  return (\n    <Box\n      flexDirection=\"column\"\n      aria-role=\"list\"\n      aria-state={{ disabled: disabled || undefined }}\n    >\n      <Text aria-label={ariaLabel}>{\"\"}</Text>\n      {label && (\n        <Text bold color={theme.colors.primary}>\n          {label}\n        </Text>\n      )}\n      {visibleLines.map((line, idx) => {\n        const isCursor = idx === cursor;\n        const spaces = \" \".repeat(line.depth * indent);\n\n        if (line.type === \"open\") {\n          const isCollapsed =\n            collapsedPaths.has(line.path) ||\n            (collapsed && collapsedPaths.size === 0 && line.depth === 0);\n          return (\n            <Box\n              key={line.path + idx}\n              aria-role=\"listitem\"\n              aria-label={`${line.key ?? \"value\"}, level ${line.depth + 1}, ${isCollapsed ? \"collapsed\" : \"expanded\"}${isCursor && isFocused ? \", current\" : \"\"}`}\n              aria-state={{\n                expanded: !isCollapsed,\n                selected: isCursor && isFocused,\n              }}\n            >\n              <Text\n                backgroundColor={isCursor ? theme.colors.selection : undefined}\n              >\n                <Text>{spaces}</Text>\n                {line.key !== undefined && (\n                  <>\n                    <Text color={theme.colors.primary}>\n                      &quot;{line.key}&quot;\n                    </Text>\n                    <Text color={theme.colors.foreground}>: </Text>\n                  </>\n                )}\n                <Text color={theme.colors.foreground}>{line.openChar}</Text>\n                {isCollapsed && (\n                  <>\n                    <Text color={theme.colors.mutedForeground}> ... </Text>\n                    <Text color={theme.colors.foreground}>\n                      {line.closeChar}\n                    </Text>\n                    {!line.isLast && (\n                      <Text color={theme.colors.foreground}>,</Text>\n                    )}\n                  </>\n                )}\n              </Text>\n            </Box>\n          );\n        }\n\n        if (line.type === \"close\") {\n          return (\n            <Box key={line.path + idx} aria-role=\"listitem\">\n              <Text\n                backgroundColor={isCursor ? theme.colors.selection : undefined}\n              >\n                <Text>{spaces}</Text>\n                <Text color={theme.colors.foreground}>{line.closeChar}</Text>\n                {!line.isLast && <Text color={theme.colors.foreground}>,</Text>}\n              </Text>\n            </Box>\n          );\n        }\n\n        return (\n          <Box\n            key={line.path + idx}\n            aria-role=\"listitem\"\n            aria-label={`${line.key ?? \"value\"}: ${String(line.value)}${isCursor && isFocused ? \", current\" : \"\"}`}\n            aria-state={{ selected: isCursor && isFocused }}\n          >\n            <Text\n              backgroundColor={isCursor ? theme.colors.selection : undefined}\n            >\n              <Text>{spaces}</Text>\n              {line.key !== undefined && (\n                <>\n                  <Text color={theme.colors.primary}>\n                    &quot;{line.key}&quot;\n                  </Text>\n                  <Text color={theme.colors.foreground}>: </Text>\n                </>\n              )}\n              {renderValue(line.value)}\n              {!line.isLast && <Text color={theme.colors.foreground}>,</Text>}\n            </Text>\n          </Box>\n        );\n      })}\n    </Box>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "data"
  ],
  "type": "registry:ui"
}