{
  "slug": "editor",
  "name": "Selection AI Editor",
  "title": "Selection AI Editor",
  "type": "registry:ui",
  "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.",
  "summary": "A selection editor built on the CSS Custom Highlight API, with a spring-positioned floating toolbar, formatting controls that stay open between toggles, an animated reasoning orb, staggered source citations, and a word-by-word reveal of the provided answer that can replace the selection.",
  "category": "ai-stuff",
  "tags": [
    "ai",
    "editor",
    "selection",
    "toolbar",
    "streaming",
    "motion",
    "highlight",
    "contenteditable",
    "spring"
  ],
  "dependencies": [
    "motion",
    "lucide-react",
    "clsx",
    "tailwind-merge"
  ],
  "version": "1.0.0",
  "createdDate": "2026-09-29",
  "updatedDate": "2026-09-29",
  "interactive": true,
  "supportsColor": false,
  "highlights": [
    "Precise cursor-targeted anchoring that calculates exact pointerup release coordinates rather than ambiguous multi-line centers",
    "Formatting persistence allowing sequential Bold, Italic, Underline and Strikethrough toggles without losing the toolbar",
    "Native CSS Highlight API (::highlight(editor-ask)) integration that keeps text selections visibly active during input focus",
    "Multi-stage thinking drawer that runs on timers: understanding the context, gathering sources, then revealing the answer",
    "Pure in-place focus dissolution on dismiss (opacity: 0, blur: 6px) eliminating jarring position snaps or scale jumps",
    "Dynamic bi-directional indicator arrow with auto-clamping boundary detection and offset calculation",
    "Replace selection, Append to document, or Copy to clipboard once the answer finishes"
  ],
  "anatomy": [
    "<Editor> (Outer relative wrapper with selectionchange listener and highlight provider)",
    "<ContentEditable> (Accessible semantic text surface with title and body paragraph)",
    "<FloatingToolbar> (Absolute-positioned pill with spring entrance and in-place dissolve exit)",
    "<IndicatorArrow> (Bi-directional directional pointer aligning directly with cursor release coordinates)",
    "<FormattingGroup> (Bold, Italic, Underline and Strikethrough with active state, plus a Code button)",
    "<AskAIButton> (Ask AI button with a static micro orb and gradient border)",
    "<PromptComposer> (Inline query input field with autofocus, enter-to-submit, and escape-to-collapse)",
    "<StageDrawer> (Expanding panel with spring height animation)",
    "<ReasoningOrb> (Animated SVG orb switching from S3 (understanding) to S2 (gathering sources))",
    "<TextShimmer> (Radiant linear gradient sweep passing over stage status indicators)",
    "<SourceCitations> (Staggered favicon badges loaded dynamically from external domains)",
    "<StreamingText> (Word-by-word reveal of the answer prop with a blinking cursor)",
    "<CompletionActions> (Replace selection, append to document, and copy-to-clipboard buttons)"
  ],
  "physics": {
    "engine": "Framer Motion Spring Physics",
    "description": "Harmonic oscillator spring dynamics (stiffness 440, damping 28, mass 0.7) for toolbar positioning, combined with pure cubic-bezier(0.16, 1, 0.3, 1) in-place blur dissipation on dismiss.",
    "parameters": [
      {
        "label": "Toolbar Spring Stiffness",
        "value": "440"
      },
      {
        "label": "Toolbar Spring Damping",
        "value": "28"
      },
      {
        "label": "Toolbar Spring Mass",
        "value": "0.7"
      },
      {
        "label": "Button Micro-Spring",
        "value": "stiffness: 520, damping: 22"
      },
      {
        "label": "Dissolve Exit Duration",
        "value": "200ms cubic-bezier(0.16, 1, 0.3, 1)"
      },
      {
        "label": "Streaming Word Cadence",
        "value": "24ms per word"
      },
      {
        "label": "Selection Settle Debounce",
        "value": "100ms"
      }
    ]
  },
  "accessibility": {
    "role": "toolbar",
    "aria": "Accessible contentEditable document with aria-label and role=\"textbox\", a role=\"toolbar\" with aria-label=\"Selection\", and labelled buttons. The text surface has a focus-visible style.",
    "keyboard": [
      {
        "key": "Enter",
        "description": "In the Ask AI input, submits the question."
      },
      {
        "key": "Escape",
        "description": "In the Ask AI input, closes the toolbar and exits Ask AI mode."
      },
      {
        "key": "Tab / Shift + Tab",
        "description": "Moves between toolbar buttons and the prompt input."
      },
      {
        "key": "Ctrl/Cmd + B",
        "description": "Browser-native bold in the editable text (not handled by the component)."
      },
      {
        "key": "Ctrl/Cmd + I",
        "description": "Browser-native italic in the editable text (not handled by the component)."
      }
    ],
    "reducedMotion": "With prefers-reduced-motion, the toolbar layout resize and the stage panel height change instantly. Other entrance, exit and hover animations still run."
  },
  "props": [
    {
      "name": "title",
      "type": "string",
      "description": "Optional title displayed above the editable document text."
    },
    {
      "name": "children",
      "type": "string",
      "required": true,
      "description": "The editable document content rendered within the contentEditable container."
    },
    {
      "name": "answer",
      "type": "string",
      "required": true,
      "description": "The simulated or provided AI response that streams into the bottom drawer."
    },
    {
      "name": "className",
      "type": "string",
      "description": "Custom CSS class applied to the outer relative editor wrapper."
    },
    {
      "name": "onAsk",
      "type": "(question: string) => void",
      "description": "Callback triggered when the user submits an AI query from the toolbar."
    }
  ],
  "usage": "import { Editor } from \"@/components/ui/editor\";\n\nexport function EditorDemo() {\n  return (\n    <Editor\n      title=\"Release notes\"\n      answer=\"The update adds keyboard support and faster loading.\"\n    >\n      Select any sentence in this paragraph and ask the assistant to rewrite it.\n    </Editor>\n  );\n}",
  "guidelines": {
    "recommended": [
      "Notion or Lex-style collaborative and AI-assisted text editors",
      "Inline AI copilots for blog writing, documentation, and markdown composers",
      "Interactive document viewers where readers highlight text to ask context questions"
    ],
    "bestPractices": [
      "Avoid nesting inside elements with overflow: hidden that might clip the floating toolbar",
      "Provide meaningful fallback styling for browsers that do not yet support the CSS Custom Highlight API",
      "Ensure contentEditable text has high color contrast in both light and dark themes"
    ]
  },
  "files": [
    {
      "name": "editor.tsx",
      "path": "registry/ui/editor.tsx",
      "type": "registry:ui",
      "target": "@ui/editor.tsx",
      "code": "\"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",
      "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"
    },
    {
      "name": "editor-orb.tsx",
      "path": "components/ui/editor-orb.tsx",
      "type": "registry:ui",
      "target": "@ui/editor-orb.tsx",
      "code": "\"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",
      "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"
    },
    {
      "name": "streaming-text.tsx",
      "path": "components/ui/streaming-text.tsx",
      "type": "registry:ui",
      "target": "@ui/streaming-text.tsx",
      "code": "\"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",
      "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"
    },
    {
      "name": "text-shimmer.tsx",
      "path": "components/ui/text-shimmer.tsx",
      "type": "registry:ui",
      "target": "@ui/text-shimmer.tsx",
      "code": "\"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",
      "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"
    }
  ],
  "$schema": "https://ui.shadcn.com/schema/registry-item.json"
}
