{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "reasoning",
  "title": "Reasoning",
  "author": "Jamie Thompson <jamie@jamiethompson.design> (https://jamiethompson.design)",
  "description": "Collapsible \"thinking\" block for AI reasoning/thought tokens. ReasoningTrigger shows a shimmering \"Thinking...\" while isStreaming, then \"Thought for N seconds\" (duration measured automatically or passed in); ReasoningContent renders the reasoning text as markdown with Streamdown. Auto-opens when streaming starts and auto-closes one second after it ends; defaultOpen={false} disables the auto-open, a reader's manual toggle is respected, and a controlled parent receives both requests through onOpenChange. Use it above a message response when the model exposes reasoning.",
  "dependencies": [
    "cn",
    "lucide-react"
  ],
  "registryDependencies": [
    "collapsible",
    "@uifiles/response"
  ],
  "files": [
    {
      "path": "registry/ai/reasoning.tsx",
      "content": "// Derived from Vercel AI Elements reasoning.tsx (Apache-2.0, Copyright 2023 Vercel, Inc.).\n// Modified for uifiles: ported from Radix UI to Base UI; dependencies point at @uifiles.\n\"use client\"\n\nimport { cn } from \"cn\"\nimport { BrainIcon, ChevronDownIcon } from \"lucide-react\"\nimport type { ComponentProps, ReactNode } from \"react\"\nimport {\n  createContext,\n  memo,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\"\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\"\nimport { MessageResponse } from \"@/registry/ai/response\"\n\ninterface ReasoningContextValue {\n  isStreaming: boolean\n  isOpen: boolean\n  setIsOpen: (open: boolean) => void\n  duration: number | undefined\n}\n\nconst ReasoningContext = createContext<ReasoningContextValue | null>(null)\n\nexport const useReasoning = () => {\n  const context = useContext(ReasoningContext)\n  if (!context) {\n    throw new Error(\"Reasoning components must be used within Reasoning\")\n  }\n  return context\n}\n\nexport type ReasoningProps = ComponentProps<typeof Collapsible> & {\n  isStreaming?: boolean | undefined\n  open?: boolean | undefined\n  defaultOpen?: boolean | undefined\n  onOpenChange?: ((open: boolean) => void) | undefined\n  duration?: number | undefined\n}\n\nconst AUTO_CLOSE_DELAY = 1000\nconst MS_IN_S = 1000\n\nexport const Reasoning = memo(\n  ({\n    className,\n    isStreaming = false,\n    open,\n    defaultOpen,\n    onOpenChange,\n    duration: durationProp,\n    children,\n    ...props\n  }: ReasoningProps) => {\n    const resolvedDefaultOpen = defaultOpen ?? isStreaming\n    // Track if defaultOpen was explicitly set to false (to prevent auto-open)\n    const isExplicitlyClosed = defaultOpen === false\n\n    // Controlled/uncontrolled open state (replaces Radix useControllableState).\n    // The latest onOpenChange lives in a ref so the setter stays stable and\n    // the effects below do not restart the auto-close timer whenever a parent\n    // re-renders with a new inline handler.\n    const onOpenChangeRef = useRef(onOpenChange)\n    useEffect(() => {\n      onOpenChangeRef.current = onOpenChange\n    })\n    const [uncontrolledOpen, setUncontrolledOpen] =\n      useState(resolvedDefaultOpen)\n    const isOpenControlled = open !== undefined\n    const isOpen = isOpenControlled ? open : uncontrolledOpen\n    const setIsOpen = useCallback(\n      (next: boolean) => {\n        if (!isOpenControlled) setUncontrolledOpen(next)\n        onOpenChangeRef.current?.(next)\n      },\n      [isOpenControlled]\n    )\n\n    // Controlled/uncontrolled duration (replaces Radix useControllableState)\n    const [uncontrolledDuration, setUncontrolledDuration] = useState<\n      number | undefined\n    >(undefined)\n    const duration = durationProp ?? uncontrolledDuration\n\n    const hasEverStreamedRef = useRef(isStreaming)\n    const wasStreamingRef = useRef(false)\n    // Set once the auto-close has fired or the reader has toggled the panel\n    // themselves; reset when a new stream starts.\n    const autoCloseSpentRef = useRef(false)\n    const startTimeRef = useRef<number | null>(null)\n\n    // Track when streaming starts and compute duration\n    useEffect(() => {\n      if (isStreaming) {\n        hasEverStreamedRef.current = true\n        if (startTimeRef.current === null) {\n          startTimeRef.current = Date.now()\n        }\n      } else if (startTimeRef.current !== null) {\n        // At least one second: a stream whose start and end commits land in\n        // the same millisecond would otherwise measure 0, which the trigger\n        // reads as \"still thinking\" and shimmers forever.\n        setUncontrolledDuration(\n          Math.max(1, Math.ceil((Date.now() - startTimeRef.current) / MS_IN_S))\n        )\n        startTimeRef.current = null\n      }\n    }, [isStreaming])\n\n    // Auto-open only when a stream starts (unless explicitly closed), so a\n    // reader who closes the panel mid-stream is not re-opened on the next render.\n    useEffect(() => {\n      const started = isStreaming && !wasStreamingRef.current\n      wasStreamingRef.current = isStreaming\n      if (!started) return\n      autoCloseSpentRef.current = false\n      if (!isOpen && !isExplicitlyClosed) {\n        setIsOpen(true)\n      }\n    }, [isStreaming, isOpen, isExplicitlyClosed, setIsOpen])\n\n    // Auto-close once, one second after streaming ends: only if it ever\n    // streamed (#86) and the reader has not taken the panel over.\n    useEffect(() => {\n      if (\n        !hasEverStreamedRef.current ||\n        isStreaming ||\n        !isOpen ||\n        autoCloseSpentRef.current\n      ) {\n        return\n      }\n      const timer = setTimeout(() => {\n        autoCloseSpentRef.current = true\n        setIsOpen(false)\n      }, AUTO_CLOSE_DELAY)\n\n      return () => clearTimeout(timer)\n    }, [isStreaming, isOpen, setIsOpen])\n\n    // A manual toggle hands the panel to the reader until the next stream.\n    const handleOpenChange = useCallback(\n      (newOpen: boolean) => {\n        autoCloseSpentRef.current = true\n        setIsOpen(newOpen)\n      },\n      [setIsOpen]\n    )\n\n    const contextValue = useMemo(\n      () => ({ duration, isOpen, isStreaming, setIsOpen: handleOpenChange }),\n      [duration, isOpen, isStreaming, handleOpenChange]\n    )\n\n    return (\n      <ReasoningContext.Provider value={contextValue}>\n        <Collapsible\n          className={cn(\"not-prose mb-4\", className)}\n          onOpenChange={handleOpenChange}\n          open={isOpen}\n          {...props}\n        >\n          {children}\n        </Collapsible>\n      </ReasoningContext.Provider>\n    )\n  }\n)\n\nexport type ReasoningTriggerProps = ComponentProps<\n  typeof CollapsibleTrigger\n> & {\n  getThinkingMessage?:\n    ((isStreaming: boolean, duration?: number) => ReactNode) | undefined\n}\n\nconst defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {\n  if (isStreaming || duration === 0) {\n    return <span className=\"shimmer [--shimmer-duration:1s]\">Thinking...</span>\n  }\n  if (duration === undefined) {\n    return <span>Thought for a few seconds</span>\n  }\n  return <span>Thought for {duration} seconds</span>\n}\n\nexport const ReasoningTrigger = memo(\n  ({\n    className,\n    children,\n    getThinkingMessage = defaultGetThinkingMessage,\n    ...props\n  }: ReasoningTriggerProps) => {\n    const { isStreaming, isOpen, duration } = useReasoning()\n\n    return (\n      <CollapsibleTrigger\n        className={cn(\n          \"flex min-h-6 w-full items-center gap-2 text-sm text-muted-foreground transition-colors hover:text-foreground\",\n          className\n        )}\n        {...props}\n      >\n        {children ?? (\n          <>\n            <BrainIcon className=\"size-4\" />\n            {getThinkingMessage(isStreaming, duration)}\n            <ChevronDownIcon\n              className={cn(\n                \"size-4 transition-transform\",\n                isOpen ? \"rotate-180\" : \"rotate-0\"\n              )}\n            />\n          </>\n        )}\n      </CollapsibleTrigger>\n    )\n  }\n)\n\nexport type ReasoningContentProps = ComponentProps<\n  typeof CollapsibleContent\n> & {\n  children: string\n}\n\nexport const ReasoningContent = memo(\n  ({ className, children, ...props }: ReasoningContentProps) => (\n    <CollapsibleContent\n      className={cn(\n        \"mt-4 text-sm\",\n        \"text-muted-foreground outline-none data-open:animate-in data-open:slide-in-from-top-2 data-closed:animate-out data-closed:fade-out-0 data-closed:slide-out-to-top-2\",\n        className\n      )}\n      {...props}\n    >\n      {/* uifiles: Message Response carries the plugins and the high-contrast\n          shiki pair, and adds what bare Streamdown lacks: overflowing code,\n          tables and formulas become named tab stops, and links confirm in an\n          accessible dialog. */}\n      <MessageResponse>{children}</MessageResponse>\n    </CollapsibleContent>\n  )\n)\n\nReasoning.displayName = \"Reasoning\"\nReasoningTrigger.displayName = \"ReasoningTrigger\"\nReasoningContent.displayName = \"ReasoningContent\"\n",
      "type": "registry:component",
      "target": "components/ai/reasoning.tsx"
    }
  ],
  "docs": "Derived from Vercel AI Elements (Apache-2.0, Copyright 2023 Vercel, Inc.); ported to Base UI and maintained by uifiles. Built for the Base UI styles (base-nova): it composes the Base UI wrappers from components/ui and relies on their render prop and event signatures, so it does not work on Radix or React Aria styles. Ported to Base UI. @radix-ui/react-use-controllable-state is replaced with a useState controlled/uncontrolled pattern for both open and duration (props unchanged); the latest onOpenChange is kept in a ref so a parent that re-renders with an inline handler does not restart the auto-close timer. The AI Elements <Shimmer> dependency is gone: the thinking label is a <span className=\"shimmer [--shimmer-duration:1s]\"> using the shimmer utility from shadcn/tailwind.css, and the finished label is a <span> (upstream used <p>, which is invalid inside a button). Collapsible content selectors use data-open/data-closed instead of data-[state=...]. onOpenChange receives only (open) as upstream; Base UI's eventDetails argument is dropped. Auto-open fires only when isStreaming turns true (upstream re-opened the panel on every render while streaming, so it could not be closed mid-stream); a manual toggle takes the panel out of the auto-open/auto-close cycle until the next stream starts, and each new stream auto-opens and auto-closes once (upstream never auto-closed a second stream). The measured duration is at least 1 second, so a stream that starts and ends within the same millisecond shows \"Thought for 1 seconds\" rather than upstream's permanent \"Thinking...\" (duration 0 is the trigger's streaming sentinel; pass duration={0} only to mean still thinking). A controlled parent receives the auto-open and auto-close requests through onOpenChange. ReasoningContent renders the reasoning text with MessageResponse from @uifiles/response (installed with this item) instead of upstream's bare Streamdown, so reasoning content is rendered by Message Response and shares its behavior: the cjk, code, math and mermaid plugins; the github-light-high-contrast/github-dark-high-contrast shiki pair (Streamdown's default GitHub light theme fails AA on orange tokens); its named scroll regions (a code block, table or display formula that overflows becomes a keyboard tab stop, role=\"group\" named \"Code\", \"Table\" or \"Math\"); its KaTeX requirement (without KaTeX's stylesheet every formula renders twice); and its link-safety dialog (a native modal <dialog> named \"Open external link?\" that takes focus and gives it back to the link). The trigger row is min-h-6 (24px) to meet the WCAG 2.2 target size. CSS: the CLI adds @uifiles/response's stylesheet lines to your globals.css with this item: `@import \"streamdown/styles.css\"`, `@import \"katex/dist/katex.min.css\"` and the `.katex-display` overflow rule. Add `@source \"../node_modules/streamdown/dist/*.js\"` yourself, next to your `@import \"tailwindcss\"` and with the path relative to your CSS file, so Streamdown's Tailwind classes compile. See @uifiles/response for what each line does.",
  "categories": [
    "ai",
    "chat"
  ],
  "type": "registry:component"
}