{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "editor",
  "type": "registry:ui",
  "title": "Selection AI Editor",
  "description": "Contextual floating AI toolbar and formatting bar that anchors to text selections, with simulated multi-stage reasoning, word-by-word answer reveal and inline editing.",
  "dependencies": [
    "motion",
    "lucide-react",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/editor.tsx",
      "content": "\"use client\";\n\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n  ArrowRight as IconArrowRight,\n  Bold as IconBold,\n  Code as IconCode,\n  Italic as IconItalic,\n  Strikethrough as IconStrikethrough,\n  Underline as IconUnderline,\n  X as IconClose,\n  Check as IconCheck,\n  Copy as IconCopy,\n  CornerDownLeft as IconInsert,\n  RotateCcw as IconReset,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { Orb, EditorOrb, type OrbVariant } from \"@/components/ui/editor-orb\";\nimport { StreamingText } from \"@/components/ui/streaming-text\";\nimport { TextShimmer } from \"@/components/ui/text-shimmer\";\nimport { cn } from \"@/lib/utils\";\n\nconst SPRING_TRANSITION = {\n  type: \"spring\",\n  stiffness: 440,\n  damping: 28,\n  mass: 0.7,\n} as const;\n\nconst EXIT_TRANSITION = {\n  duration: 0.2,\n  ease: [0.16, 1, 0.3, 1],\n} as const;\n\nconst BUTTON_SPRING = {\n  type: \"spring\",\n  stiffness: 520,\n  damping: 22,\n} as const;\n\nconst SETTLE = 100;\nconst TOOLBAR_WIDTH = { base: 370, sm: 410 };\nconst TOOLBAR_GAP = 10;\nconst TOOLBAR_ROOM = 58;\nconst ANSWER_WORD_MS = 24;\n\nconst FORMATS = [\n  { name: \"Bold\", icon: IconBold, command: \"bold\" },\n  { name: \"Italic\", icon: IconItalic, command: \"italic\" },\n  { name: \"Underline\", icon: IconUnderline, command: \"underline\" },\n  { name: \"Strikethrough\", icon: IconStrikethrough, command: \"strikeThrough\" },\n  { name: \"Code\", icon: IconCode, command: \"code\" },\n] as const;\n\nconst SOURCES = [\"wikipedia.org\", \"reddit.com\", \"chatgpt.com\"];\n\nconst STAGES = {\n  understanding: { label: \"Understanding the context…\", orb: \"S3\" },\n  gathering: { label: \"Gathering sources…\", orb: \"S2\" },\n} as const satisfies Record<string, { label: string; orb: OrbVariant }>;\n\nexport interface EditorProps {\n  title?: string;\n  children: string;\n  answer: string;\n  className?: string;\n  onAsk?: (question: string) => void;\n}\n\nexport function Editor({\n  title,\n  children,\n  answer,\n  className,\n  onAsk,\n}: EditorProps) {\n  const wrap = useRef<HTMLDivElement>(null);\n  const prose = useRef<HTMLDivElement>(null);\n  const dragging = useRef(false);\n  const asking = useRef(false);\n  const isFormatting = useRef(false);\n  const savedRange = useRef<Range | null>(null);\n  const lastPointerPosition = useRef<{ x: number; y: number } | null>(null);\n\n  const [at, setAt] = useState<{\n    x: number;\n    y: number;\n    width: number;\n    arrowOffset: number;\n    placeBottom: boolean;\n  } | null>(null);\n  const [askMode, setAskMode] = useState(false);\n  const [activeFormats, setActiveFormats] = useState<string[]>([]);\n\n  const updateActiveFormats = useCallback(() => {\n    const active: string[] = [];\n    if (typeof document !== \"undefined\") {\n      if (document.queryCommandState(\"bold\")) active.push(\"Bold\");\n      if (document.queryCommandState(\"italic\")) active.push(\"Italic\");\n      if (document.queryCommandState(\"underline\")) active.push(\"Underline\");\n      if (document.queryCommandState(\"strikeThrough\"))\n        active.push(\"Strikethrough\");\n    }\n    setActiveFormats(active);\n  }, []);\n\n  const enterAskMode = useCallback(() => {\n    const selection = window.getSelection();\n    if (selection && !selection.isCollapsed) {\n      const range = selection.getRangeAt(0);\n      savedRange.current = range.cloneRange();\n      if (\n        typeof CSS !== \"undefined\" &&\n        \"highlights\" in CSS &&\n        typeof Highlight !== \"undefined\"\n      ) {\n        try {\n          CSS.highlights.set(\"editor-ask\", new Highlight(range.cloneRange()));\n        } catch {}\n      }\n    }\n    asking.current = true;\n    setAskMode(true);\n  }, []);\n\n  const exitAskMode = useCallback(() => {\n    if (typeof CSS !== \"undefined\" && \"highlights\" in CSS) {\n      try {\n        CSS.highlights.delete(\"editor-ask\");\n      } catch {}\n    }\n    asking.current = false;\n    setAskMode(false);\n  }, []);\n\n  const handleFormat = (_name: string, command: string) => {\n    if (!prose.current) return;\n    isFormatting.current = true;\n\n    const selection = window.getSelection();\n    if (savedRange.current && (!selection || selection.isCollapsed)) {\n      selection?.removeAllRanges();\n      selection?.addRange(savedRange.current);\n    }\n\n    if (command === \"code\") {\n      const sel = window.getSelection();\n      if (sel && !sel.isCollapsed) {\n        const range = sel.getRangeAt(0);\n        const selectedText = range.toString();\n        const codeNode = document.createElement(\"code\");\n        codeNode.className =\n          \"rounded-md bg-muted px-1.5 py-0.5 font-mono text-xs font-semibold text-foreground border border-border/60\";\n        codeNode.textContent = selectedText;\n        range.deleteContents();\n        range.insertNode(codeNode);\n        savedRange.current = range.cloneRange();\n      }\n    } else {\n      document.execCommand(command, false);\n      const sel = window.getSelection();\n      if (sel && !sel.isCollapsed) {\n        savedRange.current = sel.getRangeAt(0).cloneRange();\n      }\n    }\n\n    updateActiveFormats();\n\n    setTimeout(() => {\n      isFormatting.current = false;\n    }, 150);\n  };\n\n  const handleReplaceSelection = (replacement: string) => {\n    if (savedRange.current) {\n      savedRange.current.deleteContents();\n      savedRange.current.insertNode(document.createTextNode(replacement));\n      exitAskMode();\n      setAt(null);\n    }\n  };\n\n  const handleInsertBelow = (textToInsert: string) => {\n    if (prose.current) {\n      const p = document.createElement(\"p\");\n      p.className =\n        \"mt-3 text-sm leading-relaxed text-foreground bg-accent/30 p-3 rounded-xl border border-border/50\";\n      p.textContent = textToInsert;\n      prose.current.appendChild(p);\n      exitAskMode();\n      setAt(null);\n    }\n  };\n\n  useEffect(() => {\n    let timer = 0;\n\n    const show = () => {\n      const selection = window.getSelection();\n      const proseEl = prose.current;\n      const wrapEl = wrap.current;\n      if (\n        !selection ||\n        selection.isCollapsed ||\n        !proseEl ||\n        !wrapEl ||\n        !proseEl.contains(selection.anchorNode) ||\n        !proseEl.contains(selection.focusNode)\n      ) {\n        if (!isFormatting.current && !asking.current) {\n          setAt(null);\n        }\n        return;\n      }\n\n      const range = selection.getRangeAt(0);\n      savedRange.current = range.cloneRange();\n      const origin = wrapEl.getBoundingClientRect();\n      const rects = range.getClientRects();\n      const box = range.getBoundingClientRect();\n      const firstRect = rects[0] ?? box;\n      const lastRect = rects[rects.length - 1] ?? box;\n\n      const targetX = lastPointerPosition.current\n        ? lastPointerPosition.current.x - origin.left\n        : rects.length > 0\n          ? lastRect.right - origin.left\n          : box.left + box.width / 2 - origin.left;\n\n      const width = Math.min(\n        origin.width,\n        window.matchMedia(\"(min-width: 640px)\").matches\n          ? TOOLBAR_WIDTH.sm\n          : TOOLBAR_WIDTH.base,\n      );\n      const left = Math.max(\n        0,\n        Math.min(origin.width - width, targetX - width / 2),\n      );\n      const arrowLimit = width / 2 - 16;\n      const arrowOffset = Math.max(\n        -arrowLimit,\n        Math.min(arrowLimit, targetX - left - width / 2),\n      );\n      const selectionTop = firstRect.top - origin.top;\n      const placeBottom = selectionTop < TOOLBAR_ROOM;\n      const y = placeBottom\n        ? lastRect.bottom - origin.top + TOOLBAR_GAP\n        : origin.height - selectionTop + TOOLBAR_GAP;\n\n      setAt({ x: left, y, width, arrowOffset, placeBottom });\n      updateActiveFormats();\n    };\n\n    const onSelectionChange = () => {\n      if (asking.current || isFormatting.current) return;\n      const selection = window.getSelection();\n      if (!selection || selection.isCollapsed) {\n        window.clearTimeout(timer);\n        setAt(null);\n        return;\n      }\n      window.clearTimeout(timer);\n      if (!dragging.current) timer = window.setTimeout(show, SETTLE);\n    };\n\n    const onPointerDown = (event: PointerEvent) => {\n      if ((event.target as Element).closest(\"[data-editor-toolbar]\")) return;\n      exitAskMode();\n      dragging.current = true;\n      window.clearTimeout(timer);\n    };\n\n    const onPointerUp = (event: PointerEvent) => {\n      if ((event.target as Element).closest(\"[data-editor-toolbar]\")) return;\n      lastPointerPosition.current = { x: event.clientX, y: event.clientY };\n      dragging.current = false;\n      show();\n    };\n\n    document.addEventListener(\"selectionchange\", onSelectionChange);\n    document.addEventListener(\"pointerdown\", onPointerDown);\n    document.addEventListener(\"pointerup\", onPointerUp);\n    document.addEventListener(\"pointercancel\", onPointerUp);\n\n    return () => {\n      window.clearTimeout(timer);\n      document.removeEventListener(\"selectionchange\", onSelectionChange);\n      document.removeEventListener(\"pointerdown\", onPointerDown);\n      document.removeEventListener(\"pointerup\", onPointerUp);\n      document.removeEventListener(\"pointercancel\", onPointerUp);\n      if (typeof CSS !== \"undefined\" && \"highlights\" in CSS) {\n        try {\n          CSS.highlights.delete(\"editor-ask\");\n        } catch {}\n      }\n    };\n  }, [exitAskMode, updateActiveFormats]);\n\n  return (\n    <div\n      ref={wrap}\n      className={cn(\"relative selection:bg-primary/20\", className)}\n    >\n      <style>{`\n        ::highlight(editor-ask) {\n          background-color: rgba(99, 102, 241, 0.28);\n          color: inherit;\n          border-radius: 4px;\n        }\n      `}</style>\n\n      <div\n        ref={prose}\n        contentEditable\n        suppressContentEditableWarning\n        spellCheck={false}\n        role=\"textbox\"\n        aria-multiline=\"true\"\n        aria-label={title ?? \"Document\"}\n        className=\"min-h-37.5 rounded-2xl border border-border/60 bg-card p-6 outline-none transition-colors focus-visible:border-primary/50 shadow-xs\"\n      >\n        {title ? (\n          <h3 className=\"font-serif text-2xl font-semibold tracking-tight text-foreground\">\n            {title}\n          </h3>\n        ) : null}\n\n        <p\n          className={cn(\n            \"text-sm leading-relaxed text-muted-foreground\",\n            title && \"mt-4\",\n          )}\n        >\n          {children}\n        </p>\n      </div>\n\n      <AnimatePresence mode=\"wait\">\n        {at && (\n          <motion.div\n            key=\"toolbar\"\n            className=\"absolute z-50 pointer-events-auto\"\n            style={{\n              left: at.x,\n              width: at.width,\n              ...(at.placeBottom ? { top: at.y } : { bottom: at.y }),\n            }}\n            initial={{\n              opacity: 0,\n              scale: 0.96,\n              y: at.placeBottom ? -6 : 6,\n              filter: \"blur(4px)\",\n            }}\n            animate={{\n              opacity: 1,\n              scale: 1,\n              y: 0,\n              filter: \"blur(0px)\",\n            }}\n            exit={{\n              opacity: 0,\n              filter: \"blur(6px)\",\n              transition: EXIT_TRANSITION,\n            }}\n            transition={SPRING_TRANSITION}\n          >\n            <SelectionToolbar\n              askMode={askMode}\n              answer={answer}\n              arrowOffset={at.arrowOffset}\n              placeBottom={at.placeBottom}\n              onAsk={enterAskMode}\n              onClose={() => {\n                exitAskMode();\n                setAt(null);\n              }}\n              activeFormats={activeFormats}\n              onFormat={handleFormat}\n              onSubmitQuestion={onAsk}\n              onReplace={handleReplaceSelection}\n              onInsertBelow={handleInsertBelow}\n            />\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\ninterface SelectionToolbarProps {\n  askMode: boolean;\n  answer: string;\n  arrowOffset?: number;\n  placeBottom?: boolean;\n  onAsk: () => void;\n  onClose: () => void;\n  activeFormats: string[];\n  onFormat: (name: string, command: string) => void;\n  onSubmitQuestion?: (question: string) => void;\n  onReplace?: (text: string) => void;\n  onInsertBelow?: (text: string) => void;\n}\n\nfunction SelectionToolbar({\n  askMode,\n  answer,\n  arrowOffset = 0,\n  placeBottom = false,\n  onAsk,\n  onClose,\n  activeFormats,\n  onFormat,\n  onSubmitQuestion,\n  onReplace,\n  onInsertBelow,\n}: SelectionToolbarProps) {\n  const input = useRef<HTMLInputElement>(null);\n  const reduceMotion = useReducedMotion() ?? false;\n\n  const [query, setQuery] = useState<string | null>(null);\n  const [stage, setStage] = useState<keyof typeof STAGES>(\"understanding\");\n  const [answering, setAnswering] = useState(false);\n  const [shown, setShown] = useState(1);\n  const [copied, setCopied] = useState(false);\n\n  const words = React.useMemo(() => answer.split(\" \"), [answer]);\n\n  useEffect(() => {\n    if (!query) return;\n\n    const stageTimer = window.setTimeout(() => setStage(\"gathering\"), 1800);\n    const answerTimer = window.setTimeout(() => setAnswering(true), 3600);\n\n    return () => {\n      window.clearTimeout(stageTimer);\n      window.clearTimeout(answerTimer);\n    };\n  }, [query]);\n\n  useEffect(() => {\n    if (!answering || shown >= words.length) return;\n    const timer = window.setTimeout(\n      () => setShown((count) => count + 1),\n      ANSWER_WORD_MS,\n    );\n    return () => window.clearTimeout(timer);\n  }, [answering, shown, words.length]);\n\n  const submit = () => {\n    const value = input.current?.value.trim();\n    if (!value || query) return;\n    setQuery(value);\n    onSubmitQuestion?.(value);\n    input.current?.blur();\n  };\n\n  const handleCopy = () => {\n    if (!answer) return;\n    navigator.clipboard.writeText(answer);\n    setCopied(true);\n    setTimeout(() => setCopied(false), 1600);\n  };\n\n  const handleReset = () => {\n    setQuery(null);\n    setStage(\"understanding\");\n    setAnswering(false);\n    setShown(1);\n    if (input.current) {\n      input.current.value = \"\";\n      setTimeout(() => input.current?.focus(), 50);\n    }\n  };\n\n  const isStreamComplete = answering && shown >= words.length;\n\n  return (\n    <motion.div\n      layout\n      transition={reduceMotion ? { duration: 0 } : SPRING_TRANSITION}\n      role=\"toolbar\"\n      aria-label=\"Selection\"\n      data-editor-toolbar\n      data-ask={askMode ? \"\" : undefined}\n      data-sent={query ? \"\" : undefined}\n      onMouseDown={(event) => {\n        if ((event.target as HTMLElement).tagName !== \"INPUT\") {\n          event.preventDefault();\n        }\n      }}\n      className={cn(\n        \"group relative flex w-full flex-col overflow-hidden rounded-2xl\",\n        \"border border-border/80 bg-card/95 dark:bg-[#121215]/95 backdrop-blur-2xl\",\n        \"text-muted-foreground shadow-[0_16px_44px_-8px_rgba(0,0,0,0.22),0_4px_16px_-4px_rgba(0,0,0,0.12)]\",\n        \"dark:shadow-[0_24px_56px_-10px_rgba(0,0,0,0.85),0_0_0_1px_rgba(255,255,255,0.08)]\",\n        \"transition-colors\",\n      )}\n    >\n      <div\n        aria-hidden=\"true\"\n        style={{ left: `calc(50% + ${arrowOffset}px)` }}\n        className={cn(\n          \"pointer-events-none absolute -translate-x-1/2\",\n          placeBottom ? \"-top-1\" : \"-bottom-1\",\n        )}\n      >\n        <div\n          className={cn(\n            \"size-2.5 rotate-45 bg-card dark:bg-[#121215]\",\n            placeBottom\n              ? \"border-l border-t border-border/80\"\n              : \"border-r border-b border-border/80\",\n          )}\n        />\n      </div>\n\n      <div className=\"relative flex h-10 items-center justify-between px-2.5\">\n        <AnimatePresence mode=\"wait\" initial={false}>\n          {!askMode ? (\n            <motion.div\n              key=\"default-bar\"\n              initial={{ opacity: 0, x: -8 }}\n              animate={{ opacity: 1, x: 0 }}\n              exit={{ opacity: 0, x: -8, filter: \"blur(3px)\" }}\n              transition={SPRING_TRANSITION}\n              className=\"flex w-full items-center justify-between\"\n            >\n              <motion.button\n                whileHover={{ scale: 1.03 }}\n                whileTap={{ scale: 0.96 }}\n                transition={BUTTON_SPRING}\n                type=\"button\"\n                onClick={() => {\n                  onAsk();\n                  setTimeout(\n                    () => input.current?.focus({ preventScroll: true }),\n                    40,\n                  );\n                }}\n                className={cn(\n                  \"flex h-8 shrink-0 items-center gap-2 rounded-xl pl-2 pr-3.5 text-xs font-semibold text-foreground\",\n                  \"bg-linear-to-r from-violet-500/10 via-cyan-500/10 to-transparent\",\n                  \"border border-border/60 hover:border-violet-500/40 hover:from-violet-500/15 hover:to-cyan-500/15\",\n                  \"transition-all cursor-pointer shadow-2xs\",\n                )}\n              >\n                <EditorOrb stirring={false} />\n                <span>Ask AI</span>\n              </motion.button>\n\n              <div className=\"mx-1 h-4.5 w-px shrink-0 bg-border/80 sm:mx-2\" />\n\n              <div className=\"flex min-w-0 items-center gap-0.5 overflow-x-auto [scrollbar-width:none] sm:gap-1\">\n                {FORMATS.map(({ name, icon: Icon, command }) => {\n                  const isActive = activeFormats.includes(name);\n                  return (\n                    <motion.button\n                      key={name}\n                      whileHover={{ scale: 1.08 }}\n                      whileTap={{ scale: 0.92 }}\n                      transition={BUTTON_SPRING}\n                      type=\"button\"\n                      aria-label={name}\n                      onClick={() => onFormat(name, command)}\n                      className={cn(\n                        \"relative flex size-7.5 items-center justify-center rounded-lg transition-colors cursor-pointer\",\n                        isActive\n                          ? \"bg-foreground text-background dark:bg-white dark:text-black font-semibold shadow-xs\"\n                          : \"text-muted-foreground hover:bg-muted/80 hover:text-foreground active:bg-muted\",\n                      )}\n                    >\n                      <Icon\n                        className=\"size-3.5\"\n                        strokeWidth={isActive ? 2.6 : 2}\n                      />\n                    </motion.button>\n                  );\n                })}\n              </div>\n            </motion.div>\n          ) : !query ? (\n            <motion.div\n              key=\"input-bar\"\n              initial={{ opacity: 0, x: 10 }}\n              animate={{ opacity: 1, x: 0 }}\n              exit={{ opacity: 0, x: -10, filter: \"blur(3px)\" }}\n              transition={SPRING_TRANSITION}\n              className=\"flex w-full items-center gap-2\"\n            >\n              <EditorOrb stirring={true} className=\"ml-1\" />\n\n              <input\n                ref={input}\n                type=\"text\"\n                autoFocus\n                aria-label=\"Ask about the selection\"\n                placeholder=\"Ask anything about the selected text…\"\n                onKeyDown={(event) => {\n                  if (event.key === \"Enter\") submit();\n                  if (event.key === \"Escape\") onClose();\n                }}\n                className=\"h-8 min-w-0 flex-1 bg-transparent px-1.5 text-xs text-foreground outline-none placeholder:text-muted-foreground/70\"\n              />\n\n              <div className=\"flex items-center gap-1.5\">\n                <motion.button\n                  whileHover={{ scale: 1.06 }}\n                  whileTap={{ scale: 0.94 }}\n                  transition={BUTTON_SPRING}\n                  type=\"button\"\n                  aria-label=\"Send\"\n                  onClick={submit}\n                  className=\"flex size-7.5 items-center justify-center rounded-lg bg-foreground text-background hover:opacity-90 cursor-pointer shadow-xs\"\n                >\n                  <IconArrowRight className=\"size-3.5\" strokeWidth={2.4} />\n                </motion.button>\n                <motion.button\n                  whileHover={{ scale: 1.06 }}\n                  whileTap={{ scale: 0.94 }}\n                  transition={BUTTON_SPRING}\n                  type=\"button\"\n                  aria-label=\"Close\"\n                  onClick={onClose}\n                  className=\"flex size-7.5 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground cursor-pointer\"\n                >\n                  <IconClose className=\"size-3.5\" />\n                </motion.button>\n              </div>\n            </motion.div>\n          ) : (\n            <motion.div\n              key=\"sent-bar\"\n              initial={{ opacity: 0, y: -6 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: -6 }}\n              transition={SPRING_TRANSITION}\n              className=\"flex w-full items-center justify-between gap-2 overflow-hidden px-1\"\n            >\n              <div className=\"flex min-w-0 items-center gap-2\">\n                <EditorOrb stirring={!isStreamComplete} />\n                <span className=\"truncate text-xs font-semibold text-foreground\">\n                  {query}\n                </span>\n              </div>\n\n              <div className=\"flex shrink-0 items-center gap-1\">\n                {isStreamComplete && (\n                  <motion.button\n                    whileHover={{ scale: 1.08 }}\n                    whileTap={{ scale: 0.92 }}\n                    transition={BUTTON_SPRING}\n                    type=\"button\"\n                    aria-label=\"Ask another question\"\n                    onClick={handleReset}\n                    className=\"flex size-7.5 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground cursor-pointer\"\n                  >\n                    <IconReset className=\"size-3.5\" />\n                  </motion.button>\n                )}\n                <motion.button\n                  whileHover={{ scale: 1.08 }}\n                  whileTap={{ scale: 0.92 }}\n                  transition={BUTTON_SPRING}\n                  type=\"button\"\n                  aria-label=\"Close\"\n                  onClick={onClose}\n                  className=\"flex size-7.5 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground cursor-pointer\"\n                >\n                  <IconClose className=\"size-3.5\" />\n                </motion.button>\n              </div>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n\n      <AnimatePresence initial={false}>\n        {query && (\n          <motion.div\n            key=\"stage-container\"\n            initial={{ height: 0, opacity: 0 }}\n            animate={{ height: \"auto\", opacity: 1 }}\n            exit={{ height: 0, opacity: 0 }}\n            transition={reduceMotion ? { duration: 0 } : SPRING_TRANSITION}\n            className=\"overflow-hidden border-t border-border/70 bg-muted/30\"\n          >\n            <AnimatePresence mode=\"wait\" initial={false}>\n              {!answering ? (\n                <motion.div\n                  key=\"stage-status\"\n                  initial={{ opacity: 0, y: 6 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={{ opacity: 0, y: -6 }}\n                  transition={SPRING_TRANSITION}\n                  className=\"flex items-center gap-2.5 px-3.5 py-3\"\n                >\n                  <Orb\n                    variant={STAGES[stage].orb}\n                    label={STAGES[stage].label}\n                    size={20}\n                    className=\"shrink-0\"\n                  />\n\n                  <AnimatePresence mode=\"wait\" initial={false}>\n                    <motion.div\n                      key={stage}\n                      initial={{ opacity: 0, y: 4, filter: \"blur(2px)\" }}\n                      animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n                      exit={{ opacity: 0, y: -4, filter: \"blur(2px)\" }}\n                      transition={{ duration: 0.15, ease: \"easeOut\" }}\n                      className=\"min-w-0 flex-1 text-xs font-medium\"\n                    >\n                      <TextShimmer duration={1.5}>\n                        {STAGES[stage].label}\n                      </TextShimmer>\n                    </motion.div>\n                  </AnimatePresence>\n\n                  <div className=\"ml-auto flex items-center pl-2\">\n                    {stage === \"gathering\" &&\n                      SOURCES.map((domain, index) => (\n                        <motion.img\n                          key={domain}\n                          src={`https://www.google.com/s2/favicons?domain=${domain}&sz=64`}\n                          alt={domain}\n                          initial={{ opacity: 0, x: 10, scale: 0.4 }}\n                          animate={{ opacity: 1, x: 0, scale: 1 }}\n                          transition={{ delay: index * 0.08, ...BUTTON_SPRING }}\n                          className=\"-ml-1.5 size-5 rounded-full bg-background ring-2 ring-card first:ml-0 shadow-xs\"\n                        />\n                      ))}\n                  </div>\n                </motion.div>\n              ) : (\n                <motion.div\n                  key=\"answer-view\"\n                  initial={{ opacity: 0 }}\n                  animate={{ opacity: 1 }}\n                  transition={{ duration: 0.2 }}\n                  className=\"flex flex-col gap-3 px-4 py-3.5\"\n                >\n                  <div className=\"max-h-48 overflow-y-auto pr-1 text-xs leading-relaxed text-foreground select-text\">\n                    <StreamingText cursor={!isStreamComplete}>\n                      {words.slice(0, shown).join(\" \")}\n                    </StreamingText>\n                  </div>\n\n                  {isStreamComplete && (\n                    <motion.div\n                      initial={{ opacity: 0, y: 4 }}\n                      animate={{ opacity: 1, y: 0 }}\n                      transition={SPRING_TRANSITION}\n                      className=\"flex items-center justify-between border-t border-border/60 pt-2.5 text-[11px]\"\n                    >\n                      <div className=\"flex items-center gap-1.5\">\n                        {onReplace && (\n                          <motion.button\n                            whileHover={{ scale: 1.04 }}\n                            whileTap={{ scale: 0.95 }}\n                            transition={BUTTON_SPRING}\n                            type=\"button\"\n                            onClick={() => onReplace(answer)}\n                            className=\"flex items-center gap-1 rounded-md bg-foreground text-background px-2.5 py-1 font-medium transition-colors hover:opacity-90 cursor-pointer shadow-xs\"\n                          >\n                            <IconInsert className=\"size-3\" />\n                            <span>Replace</span>\n                          </motion.button>\n                        )}\n                        {onInsertBelow && (\n                          <motion.button\n                            whileHover={{ scale: 1.04 }}\n                            whileTap={{ scale: 0.95 }}\n                            transition={BUTTON_SPRING}\n                            type=\"button\"\n                            onClick={() => onInsertBelow(answer)}\n                            className=\"flex items-center gap-1 rounded-md border border-border bg-card px-2.5 py-1 font-medium text-foreground hover:bg-muted cursor-pointer transition-colors shadow-2xs\"\n                          >\n                            <span>Insert below</span>\n                          </motion.button>\n                        )}\n                      </div>\n\n                      <motion.button\n                        whileHover={{ scale: 1.05 }}\n                        whileTap={{ scale: 0.95 }}\n                        transition={BUTTON_SPRING}\n                        type=\"button\"\n                        onClick={handleCopy}\n                        className=\"flex items-center gap-1 rounded-md px-2 py-1 text-muted-foreground hover:bg-muted hover:text-foreground cursor-pointer transition-colors\"\n                      >\n                        {copied ? (\n                          <>\n                            <IconCheck className=\"size-3 text-emerald-500\" />\n                            <span className=\"text-emerald-500 font-medium\">\n                              Copied\n                            </span>\n                          </>\n                        ) : (\n                          <>\n                            <IconCopy className=\"size-3\" />\n                            <span>Copy</span>\n                          </>\n                        )}\n                      </motion.button>\n                    </motion.div>\n                  )}\n                </motion.div>\n              )}\n            </AnimatePresence>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </motion.div>\n  );\n}\n\nexport default Editor;\n",
      "type": "registry:ui",
      "target": "@ui/editor.tsx"
    },
    {
      "path": "src/components/ui/editor-orb.tsx",
      "content": "\"use client\";\n\nimport React from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type OrbVariant =\n  | \"S1\"\n  | \"S2\"\n  | \"S3\"\n  | \"breathing\"\n  | \"searching\"\n  | \"working\"\n  | \"solving\"\n  | \"listening\"\n  | \"connecting\"\n  | \"weaving\"\n  | \"composing\"\n  | \"shaping\"\n  | string;\n\nexport interface OrbProps {\n  variant?: OrbVariant;\n  label?: string;\n  size?: number;\n  className?: string;\n  interactive?: boolean;\n  onClick?: (event: React.MouseEvent<HTMLElement>) => void;\n}\n\nconst variantGradients: Record<\n  string,\n  { core: string; glow: string; ring: string }\n> = {\n  S1: {\n    core: \"from-emerald-400 via-teal-500 to-cyan-600\",\n    glow: \"rgba(16, 185, 129, 0.45)\",\n    ring: \"border-emerald-400/40\",\n  },\n  S2: {\n    core: \"from-cyan-400 via-sky-500 to-indigo-600\",\n    glow: \"rgba(6, 182, 212, 0.45)\",\n    ring: \"border-cyan-400/40\",\n  },\n  S3: {\n    core: \"from-violet-500 via-purple-500 to-amber-400\",\n    glow: \"rgba(139, 92, 246, 0.45)\",\n    ring: \"border-violet-400/50\",\n  },\n  breathing: {\n    core: \"from-blue-400 via-indigo-500 to-violet-600\",\n    glow: \"rgba(99, 102, 241, 0.4)\",\n    ring: \"border-blue-400/40\",\n  },\n  searching: {\n    core: \"from-cyan-400 via-blue-500 to-indigo-600\",\n    glow: \"rgba(14, 165, 233, 0.45)\",\n    ring: \"border-cyan-400/40\",\n  },\n  working: {\n    core: \"from-amber-400 via-orange-500 to-rose-500\",\n    glow: \"rgba(245, 158, 11, 0.45)\",\n    ring: \"border-amber-400/40\",\n  },\n};\n\nexport function Orb({\n  variant = \"S1\",\n  label,\n  size = 20,\n  className,\n  interactive = false,\n  onClick,\n}: OrbProps) {\n  const reduceMotion = useReducedMotion();\n  const theme = variantGradients[variant] ?? variantGradients.S3;\n\n  return (\n    <div\n      role=\"img\"\n      aria-label={label ?? `AI Orb: ${variant}`}\n      onClick={onClick}\n      style={{ width: size, height: size }}\n      className={cn(\n        \"relative flex shrink-0 items-center justify-center select-none\",\n        interactive && \"cursor-pointer hover:scale-105 transition-transform\",\n        className,\n      )}\n    >\n      <div\n        className=\"pointer-events-none absolute inset-[-30%] rounded-full blur-[6px] transition-colors duration-500\"\n        style={{ background: theme.glow }}\n      />\n\n      <motion.div\n        animate={\n          reduceMotion\n            ? { scale: 1 }\n            : {\n                scale: [0.92, 1.08, 0.92],\n                rotate: variant === \"S2\" ? [0, 180, 360] : [0, 0, 0],\n              }\n        }\n        transition={{\n          repeat: Infinity,\n          duration: variant === \"S2\" ? 3 : 2.4,\n          ease: \"easeInOut\",\n        }}\n        className={cn(\n          \"relative h-full w-full rounded-full bg-linear-to-tr shadow-inner\",\n          theme.core,\n        )}\n      >\n        <div className=\"absolute inset-0 rounded-full bg-radial from-white/70 via-transparent to-black/30\" />\n      </motion.div>\n\n      <motion.div\n        animate={\n          reduceMotion\n            ? { rotate: 0 }\n            : {\n                rotate: [0, 360],\n              }\n        }\n        transition={{\n          repeat: Infinity,\n          duration: variant === \"S3\" ? 2.5 : 4,\n          ease: \"linear\",\n        }}\n        className={cn(\n          \"pointer-events-none absolute -inset-0.5 rounded-full border border-dashed\",\n          theme.ring,\n        )}\n      />\n    </div>\n  );\n}\n\nexport function EditorOrb({\n  stirring = false,\n  className,\n}: {\n  stirring?: boolean;\n  className?: string;\n}) {\n  const reduceMotion = useReducedMotion();\n\n  return (\n    <div\n      data-stirring={stirring || undefined}\n      className={cn(\n        \"editor-orb relative flex size-5 shrink-0 items-center justify-center rounded-full overflow-hidden\",\n        className,\n      )}\n    >\n      <motion.div\n        animate={\n          reduceMotion\n            ? { rotate: 0 }\n            : stirring\n              ? { rotate: [0, 720], scale: [1, 1.15, 1] }\n              : { rotate: [0, 360] }\n        }\n        transition={{\n          repeat: Infinity,\n          duration: stirring ? 1.4 : 4,\n          ease: stirring ? \"easeInOut\" : \"linear\",\n        }}\n        className=\"absolute -inset-1 rounded-full bg-[conic-gradient(from_0deg,#8b5cf6,#06b6d4,#10b981,#f59e0b,#8b5cf6)] blur-[1px]\"\n      />\n      <div className=\"relative size-3.5 rounded-full bg-popover/90 dark:bg-card/90 shadow-sm flex items-center justify-center\">\n        <div className=\"size-1.5 rounded-full bg-linear-to-r from-violet-400 to-cyan-400\" />\n      </div>\n    </div>\n  );\n}\n\nexport default Orb;\n",
      "type": "registry:ui",
      "target": "@ui/editor-orb.tsx"
    },
    {
      "path": "src/components/ui/streaming-text.tsx",
      "content": "\"use client\";\n\nimport React from \"react\";\nimport { motion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface StreamingTextProps {\n  children?: React.ReactNode;\n  className?: string;\n  cursor?: boolean;\n}\n\nexport function StreamingText({\n  children,\n  className,\n  cursor = true,\n}: StreamingTextProps) {\n  return (\n    <span className={cn(\"inline text-foreground\", className)}>\n      {children}\n      {cursor && (\n        <motion.span\n          aria-hidden=\"true\"\n          initial={{ opacity: 0 }}\n          animate={{ opacity: [0, 1, 0] }}\n          transition={{\n            repeat: Infinity,\n            duration: 0.8,\n            ease: \"easeInOut\",\n          }}\n          className=\"ml-0.5 inline-block h-3.5 w-1.5 translate-y-0.5 rounded-xs bg-primary align-baseline\"\n        />\n      )}\n    </span>\n  );\n}\n\nexport default StreamingText;\n",
      "type": "registry:ui",
      "target": "@ui/streaming-text.tsx"
    },
    {
      "path": "src/components/ui/text-shimmer.tsx",
      "content": "\"use client\";\n\nimport React from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface TextShimmerProps {\n  children: React.ReactNode;\n  as?: React.ElementType;\n  className?: string;\n  duration?: number;\n  spread?: number;\n}\n\nexport function TextShimmer({\n  children,\n  as: Component = \"span\",\n  className,\n  duration = 1.5,\n}: TextShimmerProps) {\n  const reduceMotion = useReducedMotion();\n\n  if (reduceMotion) {\n    const Comp = Component as React.ComponentType<{\n      className?: string;\n      children?: React.ReactNode;\n    }>;\n    return (\n      <Comp className={cn(\"inline-block text-muted-foreground\", className)}>\n        {children}\n      </Comp>\n    );\n  }\n\n  return (\n    <motion.span\n      className={cn(\n        \"inline-block bg-size-[250%_100%] bg-clip-text text-transparent\",\n        \"bg-linear-to-r from-muted-foreground via-foreground to-muted-foreground\",\n        className,\n      )}\n      animate={{\n        backgroundPosition: [\"100% 0%\", \"-100% 0%\"],\n      }}\n      transition={{\n        repeat: Infinity,\n        duration,\n        ease: \"linear\",\n      }}\n    >\n      {children}\n    </motion.span>\n  );\n}\n\nexport default TextShimmer;\n",
      "type": "registry:ui",
      "target": "@ui/text-shimmer.tsx"
    }
  ]
}
