{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "reasoning-disclosure",
  "title": "Reasoning Disclosure",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A collapsible 'thinking' panel for model reasoning: shows a shimmering 'Thinking…' while streaming, opens automatically, then collapses to 'Thought for 12s' when done.",
  "dependencies": [
    "lucide-react",
    "motion@^12",
    "radix-ui"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/reasoning-disclosure.tsx",
      "content": "// Ballmac UI: Reasoning Disclosure. https://ui.ballmac.com/components/reasoning-disclosure\n\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronRight } from \"lucide-react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { Collapsible as CollapsiblePrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\n\n/** \"Thought for 12s\", \"Thought for 1m 5s\", \"Thought for a few seconds\". */\nfunction formatThoughtDuration(seconds: number) {\n  if (!Number.isFinite(seconds) || seconds < 1) return \"Thought for a few seconds\"\n  const total = Math.round(seconds)\n  if (total < 60) return `Thought for ${total}s`\n  const minutes = Math.floor(total / 60)\n  const rest = total % 60\n  return rest ? `Thought for ${minutes}m ${rest}s` : `Thought for ${minutes}m`\n}\n\nfunction ShimmerLabel({ children }: { children: React.ReactNode }) {\n  const reduceMotion = useReducedMotion()\n  if (reduceMotion) return <span>{children}</span>\n  return (\n    <motion.span\n      className=\"bg-clip-text text-transparent\"\n      style={{\n        backgroundImage:\n          \"linear-gradient(90deg, var(--muted-foreground) 35%, var(--foreground) 50%, var(--muted-foreground) 65%)\",\n        backgroundSize: \"250% 100%\",\n      }}\n      initial={{ backgroundPosition: \"100% 0%\" }}\n      animate={{ backgroundPosition: \"0% 0%\" }}\n      transition={{ duration: 1.8, repeat: Infinity, ease: \"linear\" }}\n    >\n      {children}\n    </motion.span>\n  )\n}\n\ntype ReasoningDisclosureProps = Omit<React.ComponentProps<\"div\">, \"dir\"> & {\n  /** True while the model is still reasoning. Shows \"Thinking…\" and opens the panel. */\n  streaming?: boolean\n  /** How long the model reasoned, in seconds. Shown as \"Thought for 12s\" once streaming ends. */\n  duration?: number\n  /** Controlled open state. When set, streaming no longer opens or closes the panel. */\n  open?: boolean\n  /** Initial open state when uncontrolled. Defaults to `streaming`. */\n  defaultOpen?: boolean\n  /** Called when the panel opens or closes, including the automatic open and collapse. */\n  onOpenChange?: (open: boolean) => void\n  /** Collapse automatically when streaming ends (uncontrolled only). */\n  autoCollapse?: boolean\n  /** Replaces the header text. */\n  label?: React.ReactNode\n  /** Classes for the content panel. */\n  contentClassName?: string\n}\n\nfunction ReasoningDisclosure({\n  streaming = false,\n  duration,\n  open: openProp,\n  defaultOpen,\n  onOpenChange,\n  autoCollapse = true,\n  label,\n  contentClassName,\n  className,\n  children,\n  ...props\n}: ReasoningDisclosureProps) {\n  const controlled = openProp !== undefined\n  const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? streaming)\n  const open = controlled ? openProp : internalOpen\n  const onOpenChangeRef = React.useRef(onOpenChange)\n  React.useEffect(() => {\n    onOpenChangeRef.current = onOpenChange\n  })\n\n  const setOpen = React.useCallback(\n    (next: boolean) => {\n      if (!controlled) setInternalOpen(next)\n      onOpenChangeRef.current?.(next)\n    },\n    [controlled]\n  )\n\n  // Open when streaming starts, collapse when it ends.\n  const wasStreaming = React.useRef(streaming)\n  React.useEffect(() => {\n    if (wasStreaming.current === streaming) return\n    wasStreaming.current = streaming\n    if (controlled) return\n    if (streaming) setOpen(true)\n    else if (autoCollapse) setOpen(false)\n  }, [streaming, controlled, autoCollapse, setOpen])\n\n  const heading =\n    label ??\n    (streaming ? <ShimmerLabel>Thinking…</ShimmerLabel> : duration !== undefined ? formatThoughtDuration(duration) : \"Reasoning\")\n\n  return (\n    <CollapsiblePrimitive.Root\n      data-slot=\"reasoning-disclosure\"\n      data-streaming={streaming || undefined}\n      open={open}\n      onOpenChange={setOpen}\n      className={cn(\"group/reasoning w-full text-sm\", className)}\n      {...props}\n    >\n      <CollapsiblePrimitive.Trigger\n        data-slot=\"reasoning-disclosure-trigger\"\n        className=\"-mx-1.5 inline-flex h-7 items-center gap-1.5 rounded-md px-1.5 text-muted-foreground outline-none transition-colors duration-150 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n      >\n        <span className=\"font-medium\">{heading}</span>\n        <ChevronRight\n          aria-hidden=\"true\"\n          className=\"size-3.5 transition-transform duration-150 group-data-[state=open]/reasoning:rotate-90 rtl:group-data-[state=closed]/reasoning:rotate-180 motion-reduce:transition-none\"\n        />\n      </CollapsiblePrimitive.Trigger>\n      <CollapsiblePrimitive.Content\n        data-slot=\"reasoning-disclosure-content\"\n        className={cn(\n          \"mt-1.5 border-s-2 border-border ps-4 text-[13px] leading-6 whitespace-pre-wrap break-words text-muted-foreground\",\n          contentClassName\n        )}\n      >\n        {children}\n      </CollapsiblePrimitive.Content>\n    </CollapsiblePrimitive.Root>\n  )\n}\n\nexport { ReasoningDisclosure, formatThoughtDuration, type ReasoningDisclosureProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/reasoning-disclosure.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "ai",
      "reasoning",
      "thinking",
      "collapsible",
      "llm",
      "chain-of-thought"
    ],
    "version": "1.0.0",
    "updated": "2026-09-28",
    "ai": {
      "summary": "Wrap a model's reasoning text: <ReasoningDisclosure streaming={isReasoning} duration={seconds}>{text}</ReasoningDisclosure>. It opens while streaming and collapses when streaming ends unless you control open.",
      "whenToUse": [
        "Showing reasoning or 'thinking' tokens from reasoning models above the final answer",
        "Any secondary, verbose content that should be visible while it is produced and tucked away afterwards"
      ],
      "whenNotToUse": [
        "FAQ-style expanders (use an accordion)",
        "Tool call details (use tool-call-card)"
      ],
      "composesWith": [
        "ai-message",
        "streaming-text",
        "tool-call-card"
      ],
      "a11y": [
        {
          "keys": "Enter / Space",
          "action": "Opens or closes the reasoning (aria-expanded, aria-controls)"
        },
        {
          "keys": "—",
          "action": "The shimmer is decorative and stops under reduced motion"
        }
      ],
      "customization": [
        "streaming: boolean; duration in seconds renders 'Thought for 12s' / 'Thought for 1m 5s'",
        "open/onOpenChange (controlled: streaming no longer toggles it) or defaultOpen",
        "autoCollapse={false} keeps it open after streaming ends",
        "label replaces the header text; contentClassName styles the panel",
        "AI SDK: render parts with type 'reasoning' as children; streaming while the message is streaming and no text part has started"
      ]
    },
    "examples": [
      "reasoning-disclosure-demo"
    ],
    "url": "https://ui.ballmac.com/components/reasoning-disclosure"
  },
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}