{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Collapsible AI reasoning trace that shimmers only while the model works, reports how long it took and gets out of the way.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ChevronRight } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\n\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\n/**\n * A beat before auto-collapsing. Snapping shut the instant the trace finishes\n * makes it feel like the content was yanked away mid-read.\n */\nconst AUTO_COLLAPSE_DELAY_MS = 600;\nconst MS_PER_SECOND = 1000;\nconst SHIMMER_SECONDS = 1.8;\n\nexport type AIReasoningProps = {\n  children: ReactNode;\n  className?: string;\n  /**\n   * Collapse itself once `isStreaming` goes false. The trace is scaffolding —\n   * interesting while it happens, clutter once the answer has landed.\n   */\n  collapseWhenDone?: boolean;\n  /** Start expanded. */\n  defaultOpen?: boolean;\n  /** Seconds the model spent. Omit to have the component time it itself. */\n  duration?: number;\n  isStreaming?: boolean;\n};\n\n/**\n * Collapsible reasoning trace.\n *\n * The summary line shimmers only while the model is actually working, so the\n * shimmer means something rather than being decoration. When the trace ends it\n * settles, reports how long it took, and gets out of the way.\n */\nconst AIReasoning = ({\n  children,\n  className,\n  collapseWhenDone = true,\n  defaultOpen,\n  duration,\n  isStreaming = false,\n}: AIReasoningProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [isOpen, setIsOpen] = useState(defaultOpen ?? isStreaming);\n  // Once someone opens or closes it by hand, stop deciding for them.\n  const [isUserControlled, setIsUserControlled] = useState(false);\n\n  const startedAtRef = useRef<number | null>(null);\n  const [measuredSeconds, setMeasuredSeconds] = useState<number | null>(null);\n\n  useEffect(() => {\n    if (isStreaming) {\n      startedAtRef.current = Date.now();\n      setMeasuredSeconds(null);\n      if (!isUserControlled) {\n        setIsOpen(true);\n      }\n      return;\n    }\n    if (startedAtRef.current !== null) {\n      setMeasuredSeconds((Date.now() - startedAtRef.current) / MS_PER_SECOND);\n      startedAtRef.current = null;\n    }\n  }, [isStreaming, isUserControlled]);\n\n  useEffect(() => {\n    if (isStreaming || !collapseWhenDone || isUserControlled) {\n      return;\n    }\n    const timeout = setTimeout(() => setIsOpen(false), AUTO_COLLAPSE_DELAY_MS);\n    return () => clearTimeout(timeout);\n  }, [collapseWhenDone, isStreaming, isUserControlled]);\n\n  const seconds = duration ?? measuredSeconds;\n  const summary = (() => {\n    if (isStreaming) {\n      return \"Thinking\";\n    }\n    if (seconds !== null) {\n      return `Thought for ${seconds.toFixed(1)}s`;\n    }\n    return \"Reasoning\";\n  })();\n\n  return (\n    <div className={cn(\"w-full\", className)}>\n      <button\n        aria-expanded={isOpen}\n        className=\"group flex w-full cursor-pointer items-center gap-1.5 rounded-lg py-1 text-left text-muted-foreground text-sm transition-colors hover:text-foreground\"\n        onClick={() => {\n          setIsUserControlled(true);\n          setIsOpen((current) => !current);\n        }}\n        type=\"button\"\n      >\n        <motion.span\n          animate={{ rotate: isOpen ? 90 : 0 }}\n          className=\"flex size-4 items-center justify-center\"\n          transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n        >\n          <ChevronRight aria-hidden=\"true\" size={14} />\n        </motion.span>\n\n        {/* The shimmer is the \"still working\" signal, so it exists only while\n            streaming. A permanently shimmering label is just decoration. */}\n        {isStreaming && !shouldReduceMotion ? (\n          <span\n            className=\"bg-[length:200%_100%] bg-clip-text text-transparent\"\n            style={{\n              animation: `ai-reasoning-shimmer ${SHIMMER_SECONDS}s linear infinite`,\n              backgroundImage:\n                \"linear-gradient(90deg, currentColor 0%, currentColor 35%, color-mix(in oklab, currentColor 25%, transparent) 50%, currentColor 65%, currentColor 100%)\",\n            }}\n          >\n            {summary}\n          </span>\n        ) : (\n          <span>{summary}</span>\n        )}\n      </button>\n\n      <style>{`\n        @keyframes ai-reasoning-shimmer {\n          from { background-position: 200% 0; }\n          to { background-position: -200% 0; }\n        }\n      `}</style>\n\n      <AnimatePresence initial={false}>\n        {isOpen ? (\n          <motion.div\n            animate={{ height: \"auto\", opacity: 1 }}\n            className=\"overflow-hidden\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { height: 0, opacity: 0 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { height: \"auto\", opacity: 1 }\n                : { height: 0, opacity: 0 }\n            }\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    height: SPRING_DEFAULT,\n                    opacity: { duration: 0.18, ease: EASE_OUT },\n                  }\n            }\n          >\n            {/* The rule doubles as a margin: the trace reads as an aside, not as\n                part of the answer. */}\n            <div className=\"mt-1 ml-[7px] border-border border-l pl-4 text-muted-foreground text-sm leading-relaxed\">\n              {children}\n            </div>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n};\n\nexport default AIReasoning;\n","path":"index.tsx","target":"components/smoothui/ai-reasoning/index.tsx","type":"registry:ui"}],"name":"ai-reasoning","registryDependencies":[],"title":"Ai Reasoning","type":"registry:ui"}