{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "reasoning",
  "type": "registry:component",
  "title": "Reasoning",
  "description": "Collapsible panel for assistant reasoning and thinking content.",
  "registryDependencies": [
    "collapsible",
    "https://r.assistant-ui.com/markdown-text.json"
  ],
  "dependencies": [
    "@assistant-ui/react",
    "lucide-react",
    "class-variance-authority",
    "tw-shimmer"
  ],
  "css": {
    "@import \"tw-shimmer\"": {},
    "@custom-variant data-open (&:where([data-state=\"open\"], [data-open]:not([data-open=\"false\"])))": {},
    "@custom-variant data-closed (&:where([data-state=\"closed\"], [data-closed]:not([data-closed=\"false\"])))": {},
    "@keyframes collapsible-down": {
      "from": {
        "height": "0"
      },
      "to": {
        "height": "var(--radix-collapsible-content-height, var(--collapsible-panel-height, auto))"
      }
    },
    "@keyframes collapsible-up": {
      "from": {
        "height": "var(--radix-collapsible-content-height, var(--collapsible-panel-height, auto))"
      },
      "to": {
        "height": "0"
      }
    }
  },
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/reasoning.tsx",
      "content": "\"use client\";\n\nimport {\n  createContext,\n  memo,\n  useCallback,\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { BrainIcon, ChevronDownIcon } from \"lucide-react\";\nimport {\n  useScrollLock,\n  useAuiState,\n  type ReasoningMessagePartComponent,\n  type ReasoningGroupComponent,\n} from \"@assistant-ui/react\";\nimport { MarkdownText } from \"@/components/assistant-ui/markdown-text\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/utils\";\n\nconst ANIMATION_DURATION = 200;\n\nconst ReasoningPreviewContext = createContext(false);\n\nconst reasoningVariants = cva(\"aui-reasoning-root mb-4 w-full\", {\n  variants: {\n    variant: {\n      outline: \"rounded-lg border px-3 py-2\",\n      ghost: \"\",\n      muted: \"bg-muted/50 rounded-lg px-3 py-2\",\n    },\n  },\n  defaultVariants: {\n    variant: \"outline\",\n  },\n});\n\nexport type ReasoningRootProps = Omit<\n  React.ComponentProps<typeof Collapsible>,\n  \"open\" | \"onOpenChange\"\n> &\n  VariantProps<typeof reasoningVariants> & {\n    open?: boolean;\n    onOpenChange?: (open: boolean) => void;\n    defaultOpen?: boolean;\n    /**\n     * Whether the reasoning is currently streaming. While `true` the\n     * disclosure is held open with a bottom-pinned live preview; when\n     * streaming ends it returns to `defaultOpen`, and the first manual\n     * toggle takes over the open/close state permanently. The live preview\n     * keeps following the newest tokens while the disclosure is open during\n     * streaming, even after a manual toggle, and pauses while the reader is\n     * scrolled up.\n     */\n    streaming?: boolean;\n  };\n\nfunction ReasoningRoot({\n  className,\n  variant,\n  open: controlledOpen,\n  onOpenChange: controlledOnOpenChange,\n  defaultOpen = false,\n  streaming,\n  children,\n  ...props\n}: ReasoningRootProps) {\n  const collapsibleRef = useRef<HTMLDivElement>(null);\n  const initialOpenRef = useRef(defaultOpen);\n  const [userOpen, setUserOpen] = useState<boolean | null>(null);\n  const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION);\n\n  const isControlled = controlledOpen !== undefined;\n  const isOpen = isControlled\n    ? controlledOpen\n    : (userOpen ?? (streaming || initialOpenRef.current));\n  const isPreview = streaming === true && isOpen;\n\n  const prevStreamingRef = useRef(streaming);\n  useLayoutEffect(() => {\n    if (prevStreamingRef.current === streaming) return;\n    prevStreamingRef.current = streaming;\n    // A streaming transition only animates the panel when the resting state\n    // is collapsed; with `defaultOpen` the disclosure stays open across it.\n    if (!isControlled && userOpen === null && !initialOpenRef.current) {\n      lockScroll();\n    }\n  }, [streaming, isControlled, userOpen, lockScroll]);\n\n  const handleOpenChange = useCallback(\n    (open: boolean) => {\n      lockScroll();\n      if (!isControlled) {\n        setUserOpen(open);\n      }\n      controlledOnOpenChange?.(open);\n    },\n    [lockScroll, isControlled, controlledOnOpenChange],\n  );\n\n  return (\n    <Collapsible\n      ref={collapsibleRef}\n      data-slot=\"reasoning-root\"\n      data-variant={variant}\n      open={isOpen}\n      onOpenChange={handleOpenChange}\n      className={cn(\n        \"group/reasoning-root\",\n        reasoningVariants({ variant, className }),\n      )}\n      style={\n        {\n          \"--animation-duration\": `${ANIMATION_DURATION}ms`,\n        } as React.CSSProperties\n      }\n      {...props}\n    >\n      <ReasoningPreviewContext.Provider value={isPreview}>\n        {children}\n      </ReasoningPreviewContext.Provider>\n    </Collapsible>\n  );\n}\n\nfunction ReasoningFade({\n  side = \"bottom\",\n  className,\n  ...props\n}: React.ComponentProps<\"div\"> & { side?: \"top\" | \"bottom\" }) {\n  if (side === \"top\") {\n    return (\n      <div\n        data-slot=\"reasoning-fade\"\n        className={cn(\n          \"aui-reasoning-fade pointer-events-none absolute inset-x-0 top-0 z-10 h-8\",\n          \"bg-[linear-gradient(to_bottom,var(--color-background),transparent)]\",\n          \"group-data-[variant=muted]/reasoning-root:bg-[linear-gradient(to_bottom,color-mix(in_oklab,var(--color-muted)_50%,var(--color-background)),transparent)]\",\n          \"fade-in-0 animate-in\",\n          \"duration-(--animation-duration)\",\n          className,\n        )}\n        {...props}\n      />\n    );\n  }\n\n  return (\n    <div\n      data-slot=\"reasoning-fade\"\n      className={cn(\n        \"aui-reasoning-fade pointer-events-none absolute inset-x-0 bottom-0 z-10 h-8\",\n        \"bg-[linear-gradient(to_top,var(--color-background),transparent)]\",\n        \"group-data-[variant=muted]/reasoning-root:bg-[linear-gradient(to_top,color-mix(in_oklab,var(--color-muted)_50%,var(--color-background)),transparent)]\",\n        \"fade-in-0 animate-in\",\n        \"duration-(--animation-duration)\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ReasoningTrigger({\n  active,\n  duration,\n  className,\n  ...props\n}: React.ComponentProps<typeof CollapsibleTrigger> & {\n  active?: boolean;\n  duration?: number;\n}) {\n  const durationText = duration ? ` (${duration}s)` : \"\";\n\n  return (\n    <CollapsibleTrigger\n      data-slot=\"reasoning-trigger\"\n      className={cn(\n        \"aui-reasoning-trigger group/trigger text-muted-foreground hover:text-foreground flex max-w-[75%] origin-left items-center gap-2 py-1.5 text-sm transition-[color,scale] active:scale-[0.98]\",\n        className,\n      )}\n      {...props}\n    >\n      <BrainIcon\n        data-slot=\"reasoning-trigger-icon\"\n        className=\"aui-reasoning-trigger-icon size-4 shrink-0\"\n      />\n      <span\n        data-slot=\"reasoning-trigger-label\"\n        className=\"aui-reasoning-trigger-label-wrapper relative inline-block leading-none tabular-nums\"\n      >\n        <span>Reasoning{durationText}</span>\n        {active ? (\n          <span\n            aria-hidden\n            data-slot=\"reasoning-trigger-shimmer\"\n            className=\"aui-reasoning-trigger-shimmer shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none\"\n          >\n            Reasoning{durationText}\n          </span>\n        ) : null}\n      </span>\n      <ChevronDownIcon\n        data-slot=\"reasoning-trigger-chevron\"\n        className={cn(\n          \"aui-reasoning-trigger-chevron mt-0.5 size-4 shrink-0\",\n          \"transition-transform duration-(--animation-duration) ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none\",\n          \"-rotate-90\",\n          \"group-data-open/trigger:rotate-0\",\n          \"group-data-panel-open/trigger:rotate-0\",\n        )}\n      />\n    </CollapsibleTrigger>\n  );\n}\n\nfunction ReasoningContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof CollapsibleContent>) {\n  const isPreview = useContext(ReasoningPreviewContext);\n\n  return (\n    <CollapsibleContent\n      data-slot=\"reasoning-content\"\n      className={cn(\n        \"aui-reasoning-content text-muted-foreground relative overflow-hidden text-sm outline-none\",\n        \"group/collapsible-content ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:animate-none\",\n        \"data-closed:animate-collapsible-up\",\n        \"data-open:animate-collapsible-down\",\n        \"data-closed:fill-mode-forwards\",\n        \"data-closed:pointer-events-none\",\n        \"data-open:duration-(--animation-duration)\",\n        \"data-closed:duration-(--animation-duration)\",\n        className,\n      )}\n      {...props}\n    >\n      <ReasoningFade side=\"top\" />\n      {children}\n      {isPreview ? <ReasoningFade /> : null}\n    </CollapsibleContent>\n  );\n}\n\nfunction ReasoningText({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  const isPreview = useContext(ReasoningPreviewContext);\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const contentRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (!isPreview) return;\n    const scrollEl = scrollRef.current;\n    const contentEl = contentRef.current;\n    if (!scrollEl || !contentEl) return;\n\n    let pinned = true;\n    let lastScrollTop = scrollEl.scrollTop;\n    let lastScrollHeight = scrollEl.scrollHeight;\n    const isAtBottom = () =>\n      Math.abs(\n        scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight,\n      ) <= 1 || scrollEl.scrollHeight <= scrollEl.clientHeight;\n\n    const pin = () => {\n      if (!pinned) return;\n      scrollEl.scrollTop = scrollEl.scrollHeight;\n    };\n    // A pin's own scroll event can arrive after new content grew the scroll\n    // height and read as \"not at bottom\"; only an upward move at unchanged\n    // scroll height is user intent.\n    const onScroll = () => {\n      if (isAtBottom()) {\n        pinned = true;\n      } else if (\n        scrollEl.scrollTop < lastScrollTop &&\n        scrollEl.scrollHeight === lastScrollHeight\n      ) {\n        pinned = false;\n      }\n      lastScrollTop = scrollEl.scrollTop;\n      lastScrollHeight = scrollEl.scrollHeight;\n    };\n\n    pin();\n    scrollEl.addEventListener(\"scroll\", onScroll);\n    const observer = new ResizeObserver(pin);\n    observer.observe(contentEl);\n    return () => {\n      scrollEl.removeEventListener(\"scroll\", onScroll);\n      observer.disconnect();\n    };\n  }, [isPreview]);\n\n  return (\n    <div\n      ref={scrollRef}\n      data-slot=\"reasoning-text\"\n      className={cn(\n        \"aui-reasoning-text relative z-0 max-h-64 overflow-y-auto ps-6 pt-2 pb-2 leading-relaxed text-pretty\",\n        \"transform-gpu transition-[transform,opacity] ease-[cubic-bezier(0.32,0.72,0,1)]\",\n        \"motion-reduce:animate-none\",\n        \"group-data-open/collapsible-content:animate-in\",\n        \"group-data-closed/collapsible-content:animate-out\",\n        \"group-data-open/collapsible-content:fade-in-0\",\n        \"group-data-closed/collapsible-content:fade-out-0\",\n        \"group-data-open/collapsible-content:slide-in-from-top-4\",\n        \"group-data-closed/collapsible-content:slide-out-to-top-4\",\n        \"group-data-open/collapsible-content:blur-in-[2px]\",\n        \"group-data-closed/collapsible-content:blur-out-[2px]\",\n        \"group-data-open/collapsible-content:duration-(--animation-duration)\",\n        \"group-data-closed/collapsible-content:duration-(--animation-duration)\",\n        className,\n      )}\n      {...props}\n    >\n      <div ref={contentRef} className=\"aui-reasoning-text-content space-y-4\">\n        {children}\n      </div>\n    </div>\n  );\n}\n\nconst ReasoningImpl: ReasoningMessagePartComponent = () => <MarkdownText />;\n\nconst ReasoningGroupImpl: ReasoningGroupComponent = ({\n  children,\n  startIndex,\n  endIndex,\n}) => {\n  const isReasoningStreaming = useAuiState((s) => {\n    if (s.message.status?.type !== \"running\") return false;\n    for (let index = startIndex; index <= endIndex; index++) {\n      if (s.message.parts[index]?.status.type === \"running\") return true;\n    }\n    return false;\n  });\n\n  return (\n    <ReasoningRoot streaming={isReasoningStreaming}>\n      <ReasoningTrigger active={isReasoningStreaming} />\n      <ReasoningContent aria-busy={isReasoningStreaming}>\n        <ReasoningText>{children}</ReasoningText>\n      </ReasoningContent>\n    </ReasoningRoot>\n  );\n};\n\nconst Reasoning = memo(\n  ReasoningImpl,\n) as unknown as ReasoningMessagePartComponent & {\n  Root: typeof ReasoningRoot;\n  Trigger: typeof ReasoningTrigger;\n  Content: typeof ReasoningContent;\n  Text: typeof ReasoningText;\n  Fade: typeof ReasoningFade;\n};\n\nReasoning.displayName = \"Reasoning\";\nReasoning.Root = ReasoningRoot;\nReasoning.Trigger = ReasoningTrigger;\nReasoning.Content = ReasoningContent;\nReasoning.Text = ReasoningText;\nReasoning.Fade = ReasoningFade;\n\n/**\n * @deprecated This wrapper targets the legacy `components.ReasoningGroup`\n * prop on `<MessagePrimitive.Parts>`. Use `<MessagePrimitive.GroupedParts>`\n * with a `groupBy` returning `\"group-reasoning\"` and compose `ReasoningRoot`\n * / `ReasoningTrigger` / `ReasoningContent` / `ReasoningText` directly.\n * See `thread.tsx` for an example.\n */\nconst ReasoningGroup = memo(ReasoningGroupImpl);\nReasoningGroup.displayName = \"ReasoningGroup\";\n\nexport {\n  Reasoning,\n  ReasoningGroup,\n  ReasoningRoot,\n  ReasoningTrigger,\n  ReasoningContent,\n  ReasoningText,\n  ReasoningFade,\n  reasoningVariants,\n};\n"
    }
  ]
}