{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "reasoning-steps",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["@base-ui/react", "motion", "lucide-react"],
  "devDependencies": [],
  "files": [
    {
      "path": "reasoning-steps.tsx",
      "content": "\"use client\";\n\nimport { Collapsible as CollapsiblePrimitive } from \"@base-ui/react/collapsible\";\nimport { Check } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  motion,\n  type Transition,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  type CSSProperties,\n  createContext,\n  forwardRef,\n  type HTMLAttributes,\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n// SSR-safe layout effect: \"use client\" still server-renders on first paint,\n// and useLayoutEffect warns when it runs there.\nconst useIsoLayoutEffect =\n  typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\ntype ReasoningStepStatus = \"pending\" | \"active\" | \"done\";\n\ntype StepRegistration = {\n  id: string;\n  label: ReactNode;\n  status: ReasoningStepStatus;\n};\n\ntype ReasoningStepsContextValue = {\n  activeStep: StepRegistration | undefined;\n  open: boolean;\n  reduceMotion: boolean;\n  registerStep: (id: string, info: StepRegistration) => void;\n  shownDuration: number;\n  unregisterStep: (id: string) => void;\n};\n\nconst ReasoningStepsContext = createContext<ReasoningStepsContextValue | null>(\n  null\n);\n\nfunction useReasoningStepsContext(component: string) {\n  const context = useContext(ReasoningStepsContext);\n\n  if (!context) {\n    throw new Error(`${component} must be used within <ReasoningSteps>.`);\n  }\n\n  return context;\n}\n\nconst TRIGGER_PRESS: Transition = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 30,\n  mass: 0.7,\n};\n\nconst GLYPH_SPRING: Transition = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 28,\n  mass: 0.6,\n};\n\nconst EXPAND_SPRING: Transition = {\n  type: \"spring\",\n  stiffness: 150,\n  damping: 26,\n  mass: 1.05,\n};\n\nconst COLLAPSE_SPRING: Transition = {\n  type: \"spring\",\n  stiffness: 190,\n  damping: 30,\n  mass: 1.1,\n};\n\nconst PREVIEW_SPRING: Transition = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 34,\n};\n\nconst CONTENT_EASE = [0.16, 1, 0.3, 1] as const;\nconst PREVIEW_EASE = [0.4, 0, 0.2, 1] as const;\nconst INSTANT: Transition = { duration: 0.01 };\n\n/**\n * Measures a panel's natural content height with ResizeObserver and returns a\n * concrete pixel value instead of relying on Motion's `height: \"auto\"`, which\n * resolves against the element's *visual* size — under a scaled ancestor\n * that overshoots and snaps back. `needsSnap` stays true until the first\n * measurement lands, so a panel that's already open on mount doesn't play a\n * grow-from-zero animation.\n */\nfunction useMeasuredPanelHeight(open: boolean) {\n  const innerRef = useRef<HTMLDivElement | null>(null);\n  const roRef = useRef<ResizeObserver | null>(null);\n  const [height, setHeight] = useState<number | null>(null);\n  const needsSnap = useRef(open);\n\n  const measureRef = useCallback((el: HTMLDivElement | null) => {\n    roRef.current?.disconnect();\n    roRef.current = null;\n    innerRef.current = el;\n\n    if (!el) {\n      return;\n    }\n\n    if (el.offsetHeight > 0) {\n      setHeight(el.offsetHeight);\n    }\n\n    const ro = new ResizeObserver(() => {\n      if (el.offsetHeight > 0) {\n        setHeight(el.offsetHeight);\n      }\n    });\n    ro.observe(el);\n    roRef.current = ro;\n  }, []);\n\n  useIsoLayoutEffect(() => {\n    if (open && innerRef.current && innerRef.current.offsetHeight > 0) {\n      setHeight(innerRef.current.offsetHeight);\n    }\n  }, [open]);\n\n  useEffect(() => {\n    if (height !== null) {\n      needsSnap.current = false;\n    }\n  }, [height]);\n\n  return { height, measureRef, needsSnap };\n}\n\ntype PanelRenderProps = HTMLAttributes<HTMLDivElement> & {\n  hidden?: boolean;\n  ref?: Ref<HTMLDivElement>;\n};\n\n/**\n * Shared collapsible panel for both the main disclosure and nested step\n * details. Base UI's own `hidden` would apply the instant the panel closes —\n * it can't see the Motion exit still animating — so it's dropped in favor of\n * a `hidden` we only set once the collapse has visually finished.\n */\nfunction MorphPanel({\n  children,\n  className,\n  contentClassName,\n  open,\n  reduceMotion,\n}: {\n  children: ReactNode;\n  className?: string;\n  contentClassName?: string;\n  open: boolean;\n  reduceMotion: boolean;\n}) {\n  const { height, measureRef, needsSnap } = useMeasuredPanelHeight(open);\n  const [exitComplete, setExitComplete] = useState(!open);\n\n  if (open && exitComplete) {\n    setExitComplete(false);\n  }\n\n  return (\n    <CollapsiblePrimitive.Panel\n      keepMounted\n      render={(panelProps) => {\n        const { hidden: _hidden, ...restPanelProps } =\n          panelProps as PanelRenderProps;\n\n        return (\n          <div\n            {...restPanelProps}\n            className={cn(\"overflow-hidden\", className)}\n            hidden={!open && exitComplete}\n          >\n            <motion.div\n              animate={{\n                height: open ? (height ?? \"auto\") : 0,\n                opacity: open ? 1 : 0,\n              }}\n              className=\"overflow-hidden\"\n              initial={{ height: open ? \"auto\" : 0, opacity: open ? 1 : 0 }}\n              onAnimationComplete={() => {\n                if (!open) {\n                  setExitComplete(true);\n                }\n              }}\n              transition={\n                reduceMotion || needsSnap.current\n                  ? INSTANT\n                  : open\n                    ? EXPAND_SPRING\n                    : COLLAPSE_SPRING\n              }\n            >\n              <div className={contentClassName} ref={measureRef}>\n                {children}\n              </div>\n            </motion.div>\n          </div>\n        );\n      }}\n    />\n  );\n}\n\n/**\n * Only two states reach this — pending steps never render — so it's a plain\n * dot for \"active\" that pops into a check icon for \"done\", no border, no\n * loop.\n */\nfunction StepGlyph({\n  reduceMotion,\n  status,\n}: {\n  reduceMotion: boolean;\n  status: \"active\" | \"done\";\n}) {\n  return (\n    <span className=\"flex size-3 shrink-0 items-center justify-center\">\n      <AnimatePresence initial={false} mode=\"wait\">\n        {status === \"done\" ? (\n          <motion.span\n            animate={{ scale: 1, opacity: 1 }}\n            initial={reduceMotion ? false : { scale: 0.96, opacity: 0 }}\n            key=\"done\"\n            transition={GLYPH_SPRING}\n          >\n            <Check className=\"size-3 text-foreground\" strokeWidth={3} />\n          </motion.span>\n        ) : (\n          <motion.span\n            animate={{ scale: 1, opacity: 1 }}\n            className=\"size-1.5 rounded-full bg-foreground\"\n            initial={reduceMotion ? false : { scale: 0.96, opacity: 0 }}\n            key=\"active\"\n            transition={GLYPH_SPRING}\n          />\n        )}\n      </AnimatePresence>\n    </span>\n  );\n}\n\nconst SHIMMER_TRANSITION: Transition = {\n  duration: 1.6,\n  ease: \"linear\",\n  repeat: Number.POSITIVE_INFINITY,\n};\n\n/**\n * Plain label when idle; a looping gradient sweep clipped to the text while\n * a step is active, instead of a separate status icon next to the word.\n */\nfunction ShimmerLabel({\n  active,\n  children,\n  reduceMotion,\n}: {\n  active: boolean;\n  children: ReactNode;\n  reduceMotion: boolean;\n}) {\n  if (!(active && !reduceMotion)) {\n    return (\n      <span className=\"font-medium text-[13px] text-foreground\">\n        {children}\n      </span>\n    );\n  }\n\n  return (\n    <motion.span\n      animate={{ backgroundPosition: \"0% center\" }}\n      className=\"bg-[length:250%_100%,auto] bg-clip-text font-medium text-[13px] text-transparent [--rs-base:var(--muted-foreground)] [--rs-sheen:var(--foreground)] [background-repeat:no-repeat,padding-box]\"\n      initial={{ backgroundPosition: \"100% center\" }}\n      style={\n        {\n          backgroundImage:\n            \"linear-gradient(90deg,#0000 calc(50% - 28px),var(--rs-sheen),#0000 calc(50% + 28px)), linear-gradient(var(--rs-base),var(--rs-base))\",\n        } as CSSProperties\n      }\n      transition={{ backgroundPosition: SHIMMER_TRANSITION }}\n    >\n      {children}\n    </motion.span>\n  );\n}\n\nfunction TriggerPreview({\n  activeStep,\n  reduceMotion,\n}: {\n  activeStep: StepRegistration | undefined;\n  reduceMotion: boolean;\n}) {\n  if (!activeStep) {\n    return null;\n  }\n\n  return (\n    <span className=\"mt-0.5 block overflow-hidden\">\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        <motion.span\n          animate={{ y: 0, opacity: 1, filter: \"blur(0px)\" }}\n          className=\"block truncate text-[12px] text-muted-foreground\"\n          exit={\n            reduceMotion\n              ? undefined\n              : { y: -8, opacity: 0, filter: \"blur(3px)\" }\n          }\n          initial={\n            reduceMotion ? false : { y: 8, opacity: 0, filter: \"blur(3px)\" }\n          }\n          key={activeStep.id}\n          transition={\n            reduceMotion\n              ? INSTANT\n              : {\n                  y: PREVIEW_SPRING,\n                  opacity: { duration: 0.22, ease: PREVIEW_EASE },\n                  filter: { duration: 0.22, ease: PREVIEW_EASE },\n                }\n          }\n        >\n          {activeStep.label}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\nfunction TriggerLabel({\n  activeStep,\n  isOpen,\n  reduceMotion,\n  shownDuration,\n  title,\n}: {\n  activeStep: StepRegistration | undefined;\n  isOpen: boolean;\n  reduceMotion: boolean;\n  shownDuration: number;\n  title: ReactNode;\n}) {\n  return (\n    <span className=\"min-w-0 flex-1\">\n      <span className=\"flex items-baseline gap-1.5\">\n        <ShimmerLabel\n          active={activeStep !== undefined}\n          reduceMotion={reduceMotion}\n        >\n          {title}\n        </ShimmerLabel>\n        {shownDuration > 0 ? (\n          <span className=\"text-[12px] text-muted-foreground tabular-nums\">\n            {shownDuration}s\n          </span>\n        ) : null}\n      </span>\n      {isOpen ? null : (\n        <TriggerPreview activeStep={activeStep} reduceMotion={reduceMotion} />\n      )}\n    </span>\n  );\n}\n\ninterface ReasoningStepsProps\n  extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n  children: ReactNode;\n  /** Elapsed reasoning time in seconds. Omit to let the component time itself while a step is active. */\n  duration?: number;\n  /** Initial open state for uncontrolled usage. */\n  defaultOpen?: boolean;\n  /** Controlled open state. */\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst ReasoningSteps = forwardRef<HTMLDivElement, ReasoningStepsProps>(\n  function ReasoningSteps(\n    {\n      children,\n      className,\n      defaultOpen = false,\n      duration,\n      onOpenChange,\n      open: openProp,\n      ...props\n    },\n    ref\n  ) {\n    const reduceMotion = useReducedMotion() === true;\n    const isControlled = openProp !== undefined;\n    const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);\n    const open = isControlled ? openProp : uncontrolledOpen;\n\n    const handleOpenChange = useCallback(\n      (next: boolean) => {\n        if (!isControlled) {\n          setUncontrolledOpen(next);\n        }\n        onOpenChange?.(next);\n      },\n      [isControlled, onOpenChange]\n    );\n\n    const [registry, setRegistry] = useState<Map<string, StepRegistration>>(\n      () => new Map()\n    );\n\n    const registerStep = useCallback((id: string, info: StepRegistration) => {\n      setRegistry((prev) => {\n        const existing = prev.get(id);\n        if (\n          existing &&\n          existing.status === info.status &&\n          existing.label === info.label\n        ) {\n          return prev;\n        }\n        const next = new Map(prev);\n        next.set(id, info);\n        return next;\n      });\n    }, []);\n\n    const unregisterStep = useCallback((id: string) => {\n      setRegistry((prev) => {\n        if (!prev.has(id)) {\n          return prev;\n        }\n        const next = new Map(prev);\n        next.delete(id);\n        return next;\n      });\n    }, []);\n\n    const steps = useMemo(() => Array.from(registry.values()), [registry]);\n    const activeStep = steps.find((step) => step.status === \"active\");\n    const hasActive = activeStep !== undefined;\n\n    const startTimeRef = useRef<number | null>(null);\n    const [elapsedSeconds, setElapsedSeconds] = useState(0);\n\n    useEffect(() => {\n      if (duration !== undefined || !hasActive) {\n        return;\n      }\n\n      // Keeps a running total across separate active bursts instead of\n      // restarting the clock each time.\n      if (startTimeRef.current === null) {\n        startTimeRef.current = Date.now();\n      }\n\n      const startTime = startTimeRef.current;\n      const tick = () => {\n        setElapsedSeconds(Math.round((Date.now() - startTime) / 1000));\n      };\n\n      tick();\n      const id = setInterval(tick, 1000);\n      return () => clearInterval(id);\n    }, [duration, hasActive]);\n\n    const shownDuration = duration ?? elapsedSeconds;\n\n    const contextValue = useMemo<ReasoningStepsContextValue>(\n      () => ({\n        activeStep,\n        open,\n        reduceMotion,\n        registerStep,\n        shownDuration,\n        unregisterStep,\n      }),\n      [\n        activeStep,\n        open,\n        reduceMotion,\n        registerStep,\n        shownDuration,\n        unregisterStep,\n      ]\n    );\n\n    return (\n      <CollapsiblePrimitive.Root\n        className={cn(\n          \"w-full max-w-md overflow-hidden rounded-2xl bg-card\",\n          className\n        )}\n        onOpenChange={handleOpenChange}\n        open={open}\n        ref={ref}\n        {...props}\n      >\n        <ReasoningStepsContext.Provider value={contextValue}>\n          {children}\n        </ReasoningStepsContext.Provider>\n      </CollapsiblePrimitive.Root>\n    );\n  }\n);\n\nReasoningSteps.displayName = \"ReasoningSteps\";\n\ninterface ReasoningStepsTriggerProps {\n  children?: ReactNode;\n  className?: string;\n}\n\nfunction ReasoningStepsTrigger({\n  children = \"Reasoning\",\n  className,\n}: ReasoningStepsTriggerProps) {\n  const { activeStep, reduceMotion, shownDuration } = useReasoningStepsContext(\n    \"ReasoningStepsTrigger\"\n  );\n\n  return (\n    <CollapsiblePrimitive.Trigger\n      className={cn(\n        \"flex w-full items-center gap-3 px-4 py-3 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n        className\n      )}\n      render={(triggerProps, state) => {\n        const {\n          onAnimationEnd: _onAnimationEnd,\n          onAnimationIteration: _onAnimationIteration,\n          onAnimationStart: _onAnimationStart,\n          onDrag: _onDrag,\n          onDragEnd: _onDragEnd,\n          onDragEnter: _onDragEnter,\n          onDragExit: _onDragExit,\n          onDragLeave: _onDragLeave,\n          onDragOver: _onDragOver,\n          onDragStart: _onDragStart,\n          onDrop: _onDrop,\n          ...restTriggerProps\n        } = triggerProps;\n\n        return (\n          <motion.button\n            {...restTriggerProps}\n            transition={TRIGGER_PRESS}\n            whileTap={reduceMotion ? undefined : { scale: 0.96 }}\n          >\n            <TriggerLabel\n              activeStep={activeStep}\n              isOpen={state.open}\n              reduceMotion={reduceMotion}\n              shownDuration={shownDuration}\n              title={children}\n            />\n          </motion.button>\n        );\n      }}\n    />\n  );\n}\n\ninterface ReasoningStepsContentProps {\n  children: ReactNode;\n  className?: string;\n}\n\nfunction ReasoningStepsContent({\n  children,\n  className,\n}: ReasoningStepsContentProps) {\n  const { open, reduceMotion } = useReasoningStepsContext(\n    \"ReasoningStepsContent\"\n  );\n\n  return (\n    <MorphPanel\n      className={className}\n      contentClassName=\"px-4 pt-1\"\n      open={open}\n      reduceMotion={reduceMotion}\n    >\n      {/* The \"reasoning-step-connector\" class name here must match the one\n          on each row's connector line below verbatim — Tailwind's scanner\n          needs both as static string literals, so it can't be shared via a\n          JS constant. */}\n      <ul className=\"flex flex-col [&>li:last-child_.reasoning-step-connector]:hidden\">\n        {children}\n      </ul>\n    </MorphPanel>\n  );\n}\n\ninterface ReasoningStepProps {\n  /** Stable identity; auto-generated when omitted. */\n  id?: string;\n  /** Defaults to \"done\". Steps marked \"pending\" render nothing until they flip to \"active\" or \"done\". */\n  status?: ReasoningStepStatus;\n  label: ReactNode;\n  description?: ReactNode;\n  children?: ReactNode;\n  className?: string;\n}\n\nfunction ReasoningStep({\n  children,\n  className,\n  description,\n  id: idProp,\n  label,\n  status = \"done\",\n}: ReasoningStepProps) {\n  const autoId = useId();\n  const id = idProp ?? autoId;\n  const { reduceMotion, registerStep, unregisterStep } =\n    useReasoningStepsContext(\"ReasoningStep\");\n\n  useEffect(() => {\n    if (status === \"pending\") {\n      unregisterStep(id);\n      return;\n    }\n\n    registerStep(id, { id, label, status });\n    return () => unregisterStep(id);\n  }, [id, label, registerStep, status, unregisterStep]);\n\n  if (status === \"pending\") {\n    return null;\n  }\n\n  const isActive = status === \"active\";\n\n  return (\n    <motion.li\n      animate={{ height: \"auto\", opacity: 1 }}\n      className={cn(\"overflow-hidden\", className)}\n      initial={reduceMotion ? false : { height: 0, opacity: 0 }}\n      transition={reduceMotion ? INSTANT : EXPAND_SPRING}\n    >\n      <div className=\"flex gap-3 pb-4\">\n        <div className=\"flex flex-col items-center\">\n          {/* h-5 matches the label's leading-5 line box, so the glyph\n              centers on the same line instead of sitting flush at the top. */}\n          <div className=\"flex h-5 items-center justify-center\">\n            <StepGlyph reduceMotion={reduceMotion} status={status} />\n          </div>\n          <span\n            aria-hidden=\"true\"\n            className={cn(\n              \"reasoning-step-connector my-1 w-px flex-1 rounded-full transition-colors duration-500\",\n              status === \"done\" ? \"bg-foreground/25\" : \"bg-foreground/10\"\n            )}\n          />\n        </div>\n        <div className=\"min-w-0 flex-1\">\n          <p\n            className={cn(\n              \"text-[13px] leading-5 transition-colors duration-300\",\n              isActive ? \"font-medium text-foreground\" : \"text-foreground/80\"\n            )}\n          >\n            {label}\n          </p>\n          {description ? (\n            <p className=\"mt-0.5 text-[12px] text-muted-foreground leading-5\">\n              {description}\n            </p>\n          ) : null}\n          {children}\n        </div>\n      </div>\n    </motion.li>\n  );\n}\n\ninterface ReasoningStepDetailsProps {\n  summary: ReactNode;\n  defaultOpen?: boolean;\n  children: ReactNode;\n  className?: string;\n}\n\nfunction ReasoningStepDetails({\n  children,\n  className,\n  defaultOpen = false,\n  summary,\n}: ReasoningStepDetailsProps) {\n  const { reduceMotion } = useReasoningStepsContext(\"ReasoningStepDetails\");\n  const [open, setOpen] = useState(defaultOpen);\n\n  return (\n    <CollapsiblePrimitive.Root\n      className={cn(\"-mx-1 mt-1.5\", className)}\n      onOpenChange={setOpen}\n      open={open}\n    >\n      <CollapsiblePrimitive.Trigger\n        className=\"flex items-center gap-1.5 rounded-md px-1 py-1 text-[12px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n        render={(triggerProps) => (\n          <button {...triggerProps} type=\"button\">\n            {summary}\n          </button>\n        )}\n      />\n      <MorphPanel\n        contentClassName=\"px-1 pt-1 pb-0.5\"\n        open={open}\n        reduceMotion={reduceMotion}\n      >\n        <div className=\"flex flex-col gap-1 text-[12px] text-muted-foreground leading-5\">\n          {children}\n        </div>\n      </MorphPanel>\n    </CollapsiblePrimitive.Root>\n  );\n}\n\ninterface ReasoningStepSourcesProps {\n  children: ReactNode;\n  className?: string;\n}\n\nfunction ReasoningStepSources({\n  children,\n  className,\n}: ReasoningStepSourcesProps) {\n  return (\n    <div className={cn(\"mt-2 flex flex-wrap gap-1.5\", className)}>\n      {children}\n    </div>\n  );\n}\n\ninterface ReasoningStepSourceProps {\n  children: ReactNode;\n  className?: string;\n  href?: string;\n}\n\nfunction ReasoningStepSource({\n  children,\n  className,\n  href,\n}: ReasoningStepSourceProps) {\n  const { reduceMotion } = useReasoningStepsContext(\"ReasoningStepSource\");\n  const pillClassName = cn(\n    \"inline-flex items-center gap-1 rounded-full border border-foreground/10 bg-muted/60 px-2 py-0.5 text-[11px] text-foreground/70 transition-colors\",\n    href && \"hover:bg-muted hover:text-foreground\",\n    className\n  );\n\n  if (href) {\n    return (\n      <motion.a\n        animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n        className={pillClassName}\n        href={href}\n        initial={\n          reduceMotion\n            ? false\n            : { opacity: 0, scale: 0.85, filter: \"blur(4px)\" }\n        }\n        rel=\"noreferrer noopener\"\n        target=\"_blank\"\n        transition={GLYPH_SPRING}\n      >\n        {children}\n      </motion.a>\n    );\n  }\n\n  return (\n    <motion.span\n      animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n      className={pillClassName}\n      initial={\n        reduceMotion ? false : { opacity: 0, scale: 0.85, filter: \"blur(4px)\" }\n      }\n      transition={GLYPH_SPRING}\n    >\n      {children}\n    </motion.span>\n  );\n}\n\ninterface ReasoningStepImageProps {\n  alt?: string;\n  caption?: ReactNode;\n  className?: string;\n  src: string;\n}\n\nfunction ReasoningStepImage({\n  alt = \"\",\n  caption,\n  className,\n  src,\n}: ReasoningStepImageProps) {\n  const { reduceMotion } = useReasoningStepsContext(\"ReasoningStepImage\");\n\n  return (\n    <motion.figure\n      animate={{ opacity: 1, filter: \"blur(0px)\" }}\n      className={cn(\"mt-2\", className)}\n      initial={reduceMotion ? false : { opacity: 0, filter: \"blur(4px)\" }}\n      transition={{ duration: 0.3, ease: CONTENT_EASE }}\n    >\n      {/* biome-ignore lint/performance/noImgElement: registry components stay framework-agnostic. */}\n      <img\n        alt={alt}\n        className=\"w-full max-w-[220px] rounded-lg border border-foreground/10 object-cover\"\n        height={140}\n        src={src}\n        width={220}\n      />\n      {caption ? (\n        <figcaption className=\"mt-1 text-[11px] text-muted-foreground\">\n          {caption}\n        </figcaption>\n      ) : null}\n    </motion.figure>\n  );\n}\n\nexport {\n  ReasoningSteps,\n  ReasoningStepsTrigger,\n  ReasoningStepsContent,\n  ReasoningStep,\n  ReasoningStepDetails,\n  ReasoningStepSources,\n  ReasoningStepSource,\n  ReasoningStepImage,\n};\nexport type {\n  ReasoningStepStatus,\n  ReasoningStepsProps,\n  ReasoningStepsTriggerProps,\n  ReasoningStepsContentProps,\n  ReasoningStepProps,\n  ReasoningStepDetailsProps,\n  ReasoningStepSourcesProps,\n  ReasoningStepSourceProps,\n  ReasoningStepImageProps,\n};\nexport default ReasoningSteps;\n",
      "type": "registry:ui"
    }
  ],
  "title": "Reasoning Steps",
  "description": "Base UI collapsible reasoning trace for AI replies with a shimmering trigger label and no status icon, per-step dots that pop into checkmarks once done, a self-timing elapsed-seconds readout, a crossfading collapsed preview of the current step, and a staggered vertical timeline once expanded."
}
