{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "streaming-answer",
  "type": "registry:ui",
  "title": "Streaming answer",
  "description": "Brings generated text, cited sources, and follow-up prompts into one progressive answer.",
  "dependencies": [
    "motion"
  ],
  "categories": [
    "agent"
  ],
  "docs": "https://motion-lexicon.pages.dev/en/components/streaming-answer/",
  "meta": {
    "engines": [
      "motion"
    ],
    "runtimeCost": "light",
    "signature": "Sources and follow-ups resolve after the streamed answer"
  },
  "files": [
    {
      "path": "src/registry/components/streaming-answer.tsx",
      "type": "registry:ui",
      "target": "components/motion-lexicon/streaming-answer.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useMemo, useState } from \"react\";\n\nexport type AnswerSource = {\n  id: string;\n  title: string;\n  domain: string;\n  excerpt?: string;\n};\nexport type StreamingAnswerProps = {\n  text: string;\n  sources?: readonly AnswerSource[];\n  followUps?: readonly string[];\n  speed?: number;\n  label?: string;\n  streamingLabel?: string;\n  sourcesLabel?: (count: number) => string;\n  onFollowUp?: (prompt: string) => void;\n  className?: string;\n};\n\nexport function StreamingAnswer({\n  text,\n  sources = [],\n  followUps = [],\n  speed = 34,\n  label = \"Answer\",\n  streamingLabel = \"Streaming\",\n  sourcesLabel = (count) => `${count} sources`,\n  onFollowUp,\n  className = \"\",\n}: StreamingAnswerProps) {\n  const reduced = useReducedMotion();\n  const words = useMemo(() => text.split(/\\s+/), [text]);\n  const [visible, setVisible] = useState(reduced ? words.length : 1);\n  const [sourceOpen, setSourceOpen] = useState(false);\n\n  useEffect(() => {\n    setVisible(reduced ? words.length : 1);\n    if (reduced) return;\n    const timer = window.setInterval(\n      () =>\n        setVisible((count) => {\n          if (count >= words.length) {\n            window.clearInterval(timer);\n            return count;\n          }\n          return count + 1;\n        }),\n      speed,\n    );\n    return () => window.clearInterval(timer);\n  }, [reduced, speed, words.length, text]);\n\n  const complete = visible >= words.length;\n  return (\n    <section\n      aria-label={label}\n      className={`w-full rounded-[10px] border border-neutral-200 bg-white p-4 text-neutral-950 dark:border-white/10 dark:bg-[#1b1b1b] dark:text-neutral-50 ${className}`}\n    >\n      <div className=\"flex items-center gap-2\">\n        <span className=\"size-1.5 rounded-full bg-neutral-950 dark:bg-neutral-50\" />\n        <span className=\"text-[11px] font-medium text-neutral-600 dark:text-neutral-300\">\n          {label}\n        </span>\n        {!complete ? (\n          <span className=\"ml-auto text-[10px] text-neutral-400\">\n            {streamingLabel}\n          </span>\n        ) : null}\n      </div>\n      <p className=\"mt-4 text-[14px] leading-7 tracking-[-.01em]\">\n        {words.slice(0, visible).join(\" \")}\n        {!complete ? (\n          <motion.span\n            aria-hidden=\"true\"\n            className=\"ml-1 inline-block h-[1em] w-px translate-y-0.5 bg-neutral-950 dark:bg-neutral-50\"\n            animate={{ opacity: [1, 0.15, 1] }}\n            transition={{ duration: 0.8, repeat: Infinity }}\n          />\n        ) : null}\n      </p>\n      <AnimatePresence>\n        {complete && sources.length ? (\n          <motion.div\n            initial={reduced ? false : { opacity: 0, y: 6 }}\n            animate={{ opacity: 1, y: 0 }}\n            className=\"mt-4 border-t border-neutral-100 pt-3 dark:border-white/8\"\n          >\n            <button\n              type=\"button\"\n              aria-expanded={sourceOpen}\n              onClick={() => setSourceOpen((value) => !value)}\n              className=\"flex min-h-11 items-center gap-2 rounded-lg px-2 text-[11px] font-medium hover:bg-neutral-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 dark:hover:bg-white/5\"\n            >\n              <span className=\"flex -space-x-1\">\n                {sources.slice(0, 3).map((source, index) => (\n                  <i\n                    key={source.id}\n                    className=\"grid size-5 place-items-center rounded-full border border-white bg-neutral-100 text-[8px] not-italic text-neutral-500 dark:border-[#181818] dark:bg-white/10 dark:text-neutral-300\"\n                  >\n                    {index + 1}\n                  </i>\n                ))}\n              </span>\n              {sourcesLabel(sources.length)}\n            </button>\n            <AnimatePresence initial={false}>\n              {sourceOpen ? (\n                <motion.ul\n                  initial={{ height: 0, opacity: 0 }}\n                  animate={{ height: \"auto\", opacity: 1 }}\n                  exit={{ height: 0, opacity: 0 }}\n                  className=\"mt-2 grid overflow-hidden sm:grid-cols-2\"\n                >\n                  {sources.map((source) => (\n                    <li\n                      key={source.id}\n                      className=\"rounded-lg p-2.5 hover:bg-neutral-50 dark:hover:bg-white/5\"\n                    >\n                      <strong className=\"block truncate text-[11px]\">\n                        {source.title}\n                      </strong>\n                      <span className=\"text-[10px] text-neutral-400\">\n                        {source.domain}\n                      </span>\n                    </li>\n                  ))}\n                </motion.ul>\n              ) : null}\n            </AnimatePresence>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n      {complete && followUps.length ? (\n        <div className=\"mt-3 flex flex-wrap gap-2\">\n          {followUps.map((prompt) => (\n            <button\n              key={prompt}\n              type=\"button\"\n              onClick={() => onFollowUp?.(prompt)}\n              className=\"min-h-11 rounded-lg border border-neutral-200 px-3 text-[11px] text-neutral-600 transition hover:border-neutral-400 hover:bg-neutral-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 dark:border-white/10 dark:text-neutral-300 dark:hover:bg-white/5\"\n            >\n              {prompt}\n            </button>\n          ))}\n        </div>\n      ) : null}\n    </section>\n  );\n}\n"
    }
  ]
}
