{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elements-reasoning",
  "type": "registry:component",
  "title": "Reasoning Panel",
  "description": "The props-driven reasoning disclosure: streaming preview, fades, and variants, no runtime required.",
  "registryDependencies": [
    "collapsible"
  ],
  "dependencies": [
    "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/elements/reasoning.tsx",
      "content": "\"use client\";\n\nimport {\n  createContext,\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  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/utils\";\n\nexport const 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    /** Called right before the disclosure animates, on toggle and on streaming transitions. */\n    onAnimationStart?: () => void;\n  };\n\nfunction ReasoningRoot({\n  className,\n  variant,\n  open: controlledOpen,\n  onOpenChange: controlledOnOpenChange,\n  defaultOpen = false,\n  streaming,\n  onAnimationStart,\n  children,\n  ...props\n}: ReasoningRootProps) {\n  const initialOpenRef = useRef(defaultOpen);\n  const [userOpen, setUserOpen] = useState<boolean | null>(null);\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      onAnimationStart?.();\n    }\n  }, [streaming, isControlled, userOpen, onAnimationStart]);\n\n  const handleOpenChange = useCallback(\n    (open: boolean) => {\n      onAnimationStart?.();\n      if (!isControlled) {\n        setUserOpen(open);\n      }\n      controlledOnOpenChange?.(open);\n    },\n    [onAnimationStart, isControlled, controlledOnOpenChange],\n  );\n\n  return (\n    <Collapsible\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          \"animation-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        \"animation-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={cn(\n          \"aui-reasoning-trigger-label-wrapper inline-block leading-none tabular-nums\",\n          active && \"shimmer motion-reduce:animate-none\",\n        )}\n      >\n        Reasoning{durationText}\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        \"[--tw-duration:var(--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:animation-duration-(--animation-duration)\",\n        \"group-data-closed/collapsible-content:animation-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\nexport {\n  ReasoningRoot,\n  ReasoningTrigger,\n  ReasoningContent,\n  ReasoningText,\n  ReasoningFade,\n  reasoningVariants,\n};\n"
    }
  ]
}