{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "api-response-viewer",
  "title": "API Response Viewer",
  "description": "HTTP response viewer with tabbed Body, Headers, and Timing display",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/devtools/api-response-viewer/components/elements/api-response-viewer.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype HttpStatus =\n  | \"info\"\n  | \"success\"\n  | \"redirect\"\n  | \"client-error\"\n  | \"server-error\";\n\ninterface ApiResponse {\n  status: number;\n  statusText?: string;\n  headers?: Record<string, string>;\n  body?: unknown;\n  timing?: {\n    dns?: number;\n    connect?: number;\n    ttfb?: number;\n    download?: number;\n    total: number;\n  };\n}\n\ninterface ApiResponseViewerProps {\n  response: ApiResponse;\n  defaultTab?: \"body\" | \"headers\" | \"timing\";\n  className?: string;\n}\n\nfunction getStatusType(status: number): HttpStatus {\n  if (status >= 100 && status < 200) return \"info\";\n  if (status >= 200 && status < 300) return \"success\";\n  if (status >= 300 && status < 400) return \"redirect\";\n  if (status >= 400 && status < 500) return \"client-error\";\n  return \"server-error\";\n}\n\nfunction StatusBadge({\n  status,\n  statusText,\n}: {\n  status: number;\n  statusText?: string;\n}) {\n  const type = getStatusType(status);\n  const colors: Record<HttpStatus, string> = {\n    info: \"bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300\",\n    success:\n      \"bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-300\",\n    redirect:\n      \"bg-yellow-100 text-yellow-700 dark:bg-yellow-950 dark:text-yellow-300\",\n    \"client-error\":\n      \"bg-orange-100 text-orange-700 dark:bg-orange-950 dark:text-orange-300\",\n    \"server-error\": \"bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300\",\n  };\n\n  return (\n    <span\n      role=\"status\"\n      aria-label={`HTTP status ${status}${statusText ? ` ${statusText}` : \"\"}`}\n      className={cn(\"px-2 py-0.5 rounded text-sm font-mono\", colors[type])}\n    >\n      {status} {statusText}\n    </span>\n  );\n}\n\nfunction JsonDisplay({ data }: { data: unknown }) {\n  const [collapsed, setCollapsed] = React.useState<Set<string>>(new Set());\n\n  const renderValue = (\n    value: unknown,\n    path: string,\n    depth: number,\n  ): React.ReactNode => {\n    if (value === null) {\n      return <span className=\"text-muted-foreground italic\">null</span>;\n    }\n    if (typeof value === \"boolean\") {\n      return <span className=\"text-amber-500\">{String(value)}</span>;\n    }\n    if (typeof value === \"number\") {\n      return <span className=\"text-blue-500\">{value}</span>;\n    }\n    if (typeof value === \"string\") {\n      return (\n        <span className=\"text-green-600 dark:text-green-400\">\"{value}\"</span>\n      );\n    }\n    if (Array.isArray(value)) {\n      const isCollapsed = collapsed.has(path);\n      return (\n        <span>\n          <button\n            type=\"button\"\n            onClick={() => {\n              const next = new Set(collapsed);\n              if (isCollapsed) next.delete(path);\n              else next.add(path);\n              setCollapsed(next);\n            }}\n            className=\"text-muted-foreground hover:text-foreground\"\n          >\n            {isCollapsed ? \"▶\" : \"▼\"}\n          </button>\n          <span className=\"text-muted-foreground\">[</span>\n          {isCollapsed ? (\n            <span className=\"text-muted-foreground italic text-xs mx-1\">\n              {value.length} items\n            </span>\n          ) : (\n            <div className=\"ml-4\">\n              {value.map((item, idx) => (\n                <div key={idx}>\n                  {renderValue(item, `${path}[${idx}]`, depth + 1)}\n                  {idx < value.length - 1 && (\n                    <span className=\"text-muted-foreground\">,</span>\n                  )}\n                </div>\n              ))}\n            </div>\n          )}\n          <span className=\"text-muted-foreground\">]</span>\n        </span>\n      );\n    }\n    if (typeof value === \"object\") {\n      const entries = Object.entries(value as Record<string, unknown>);\n      const isCollapsed = collapsed.has(path);\n      return (\n        <span>\n          <button\n            type=\"button\"\n            onClick={() => {\n              const next = new Set(collapsed);\n              if (isCollapsed) next.delete(path);\n              else next.add(path);\n              setCollapsed(next);\n            }}\n            className=\"text-muted-foreground hover:text-foreground\"\n          >\n            {isCollapsed ? \"▶\" : \"▼\"}\n          </button>\n          <span className=\"text-muted-foreground\">{\"{\"}</span>\n          {isCollapsed ? (\n            <span className=\"text-muted-foreground italic text-xs mx-1\">\n              {entries.length} keys\n            </span>\n          ) : (\n            <div className=\"ml-4\">\n              {entries.map(([key, val], idx) => (\n                <div key={key}>\n                  <span className=\"text-foreground\">{key}</span>\n                  <span className=\"text-muted-foreground\">: </span>\n                  {renderValue(val, `${path}.${key}`, depth + 1)}\n                  {idx < entries.length - 1 && (\n                    <span className=\"text-muted-foreground\">,</span>\n                  )}\n                </div>\n              ))}\n            </div>\n          )}\n          <span className=\"text-muted-foreground\">{\"}\"}</span>\n        </span>\n      );\n    }\n    return String(value);\n  };\n\n  return (\n    <div className=\"font-mono text-sm overflow-auto\">\n      {renderValue(data, \"$\", 0)}\n    </div>\n  );\n}\n\nfunction HeadersTable({ headers }: { headers: Record<string, string> }) {\n  return (\n    <div className=\"overflow-auto\">\n      <table className=\"w-full text-sm\">\n        <thead>\n          <tr className=\"border-b border-border\">\n            <th className=\"text-left py-2 pr-4 font-medium text-muted-foreground\">\n              Header\n            </th>\n            <th className=\"text-left py-2 font-medium text-muted-foreground\">\n              Value\n            </th>\n          </tr>\n        </thead>\n        <tbody>\n          {Object.entries(headers).map(([key, value]) => (\n            <tr key={key} className=\"border-b border-border last:border-0\">\n              <td className=\"py-2 pr-4 font-mono text-foreground\">{key}</td>\n              <td className=\"py-2 font-mono text-muted-foreground break-all\">\n                {value}\n              </td>\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  );\n}\n\nfunction TimingBar({ timing }: { timing: NonNullable<ApiResponse[\"timing\"]> }) {\n  const segments = [\n    { key: \"dns\", label: \"DNS\", color: \"bg-purple-500\", value: timing.dns },\n    {\n      key: \"connect\",\n      label: \"Connect\",\n      color: \"bg-blue-500\",\n      value: timing.connect,\n    },\n    { key: \"ttfb\", label: \"TTFB\", color: \"bg-green-500\", value: timing.ttfb },\n    {\n      key: \"download\",\n      label: \"Download\",\n      color: \"bg-orange-500\",\n      value: timing.download,\n    },\n  ].filter((s) => s.value !== undefined);\n\n  return (\n    <div className=\"space-y-4\">\n      <div className=\"h-6 flex rounded overflow-hidden\">\n        {segments.map((seg) => (\n          <div\n            key={seg.key}\n            className={cn(\n              \"flex items-center justify-center text-xs text-white\",\n              seg.color,\n            )}\n            style={{ width: `${((seg.value ?? 0) / timing.total) * 100}%` }}\n          >\n            {seg.value}ms\n          </div>\n        ))}\n      </div>\n      <div className=\"flex flex-wrap gap-4 text-sm\">\n        {segments.map((seg) => (\n          <div key={seg.key} className=\"flex items-center gap-2\">\n            <div className={cn(\"w-3 h-3 rounded\", seg.color)} />\n            <span className=\"text-muted-foreground\">{seg.label}:</span>\n            <span className=\"font-mono\">{seg.value}ms</span>\n          </div>\n        ))}\n        <div className=\"flex items-center gap-2\">\n          <div className=\"w-3 h-3 rounded bg-gray-500\" />\n          <span className=\"text-muted-foreground\">Total:</span>\n          <span className=\"font-mono font-semibold\">{timing.total}ms</span>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function ApiResponseViewer({\n  response,\n  defaultTab = \"body\",\n  className,\n}: ApiResponseViewerProps) {\n  const [activeTab, setActiveTab] = React.useState<\n    \"body\" | \"headers\" | \"timing\"\n  >(defaultTab);\n\n  const tabs = [\n    {\n      id: \"body\" as const,\n      label: \"Body\",\n      available: response.body !== undefined,\n    },\n    { id: \"headers\" as const, label: \"Headers\", available: !!response.headers },\n    { id: \"timing\" as const, label: \"Timing\", available: !!response.timing },\n  ].filter((t) => t.available);\n\n  return (\n    <div\n      data-slot=\"api-response-viewer\"\n      className={cn(\n        \"border border-border rounded-lg overflow-hidden\",\n        className,\n      )}\n    >\n      <div className=\"flex items-center justify-between px-4 py-2 border-b border-border bg-muted/50\">\n        <StatusBadge\n          status={response.status}\n          statusText={response.statusText}\n        />\n        {response.timing && (\n          <span className=\"text-sm text-muted-foreground font-mono\">\n            {response.timing.total}ms\n          </span>\n        )}\n      </div>\n\n      <div\n        className=\"flex border-b border-border\"\n        role=\"tablist\"\n        aria-label=\"Response tabs\"\n      >\n        {tabs.map((tab) => (\n          <button\n            key={tab.id}\n            type=\"button\"\n            role=\"tab\"\n            aria-selected={activeTab === tab.id}\n            aria-controls={`tabpanel-${tab.id}`}\n            onClick={() => setActiveTab(tab.id)}\n            className={cn(\n              \"px-4 py-2 text-sm font-medium transition-colors\",\n              activeTab === tab.id\n                ? \"text-foreground border-b-2 border-primary\"\n                : \"text-muted-foreground hover:text-foreground\",\n            )}\n          >\n            {tab.label}\n          </button>\n        ))}\n      </div>\n\n      <div\n        className=\"p-4\"\n        role=\"tabpanel\"\n        id={`tabpanel-${activeTab}`}\n        aria-label={`${activeTab} content`}\n      >\n        {activeTab === \"body\" && response.body !== undefined && (\n          <JsonDisplay data={response.body} />\n        )}\n        {activeTab === \"headers\" && response.headers && (\n          <HeadersTable headers={response.headers} />\n        )}\n        {activeTab === \"timing\" && response.timing && (\n          <TimingBar timing={response.timing} />\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport type { ApiResponseViewerProps, ApiResponse };\n",
      "type": "registry:component"
    }
  ],
  "docs": "Status badge with color-coded HTTP codes, collapsible JSON body, headers table, and timing waterfall.",
  "categories": [
    "devtools"
  ],
  "type": "registry:ui"
}