{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"streaming-text","type":"registry:component","title":"Streaming Text","description":"Reveals a model response as it streams in, fading each newly arrived word in on its own without replaying what's already on screen.","author":"Ryan","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/streaming-text.tsx","type":"registry:component","target":"@components/motion/streaming-text.tsx","content":"\"use client\";\n// easeui.dev/components/agents/streaming-text\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface StreamingTextProps {\n  /** The text so far. Append to it as more arrives; characters already shown never replay. */\n  text: string;\n  /** Shows a blinking cursor at the end, for while more is still on its way. Default false. */\n  streaming?: boolean;\n  className?: string;\n}\n\n/** Splits into individual characters, so each one can fade in on its own as it arrives. */\nfunction tokenize(text: string): string[] {\n  return Array.from(text);\n}\n\n/**\n * Reveals text as it streams in. Feed it a growing string and each new\n * character fades in fast enough to read as one smooth ribbon, not single\n * letters popping in. Characters already shown never replay.\n */\nexport function StreamingText({ text, streaming = false, className }: StreamingTextProps) {\n  const reduce = useReducedMotion();\n  const tokens = useMemo(() => tokenize(text), [text]);\n  // Tokens shown as of the last render. A shorter text (a new message) counts as fully settled.\n  const shown = useRef(0);\n  const settled = Math.min(tokens.length, shown.current);\n\n  useEffect(() => {\n    shown.current = tokens.length;\n  }, [tokens.length]);\n\n  return (\n    <p className={cn(\"text-sm leading-6 text-foreground\", className)}>\n      {tokens.map((token, index) => {\n        const isGlyph = /\\S/.test(token);\n        if (!isGlyph || index < settled || reduce) {\n          // biome-ignore lint/suspicious/noArrayIndexKey: streaming only appends, so a token's index never changes once shown.\n          return <span key={index}>{token}</span>;\n        }\n        return (\n          <motion.span\n            // biome-ignore lint/suspicious/noArrayIndexKey: streaming only appends, so a token's index never changes once shown.\n            key={index}\n            initial={{ opacity: 0, y: 3 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ duration: 0.12, ease: EASE_OUT }}\n          >\n            {token}\n          </motion.span>\n        );\n      })}\n      {streaming ? (\n        <span\n          aria-hidden=\"true\"\n          className=\"ml-0.5 inline-block h-3.5 w-0.5 translate-y-[2px] animate-pulse bg-foreground align-middle motion-reduce:animate-none\"\n        />\n      ) : null}\n    </p>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Micro-interactions run 100 to 150ms, standard UI\n// 150 to 250ms, and panels up to 300ms.\n// Easing curves mirror the CSS custom properties in globals.css.\n\n/** ease-out-quint. Fast start that settles quickly. Entrances, exits, feedback. */\nexport const EASE_OUT = [0.23, 1, 0.32, 1] as const;\n/** ease-in-out-cubic. Elements already on screen moving to a new spot. */\nexport const EASE_IN_OUT = [0.645, 0.045, 0.355, 1] as const;\n/** Sheet and drawer glide. */\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.23, 1, 0.32, 1)\";\n\n// Springs are described by duration and bounce, which is easier to reason about\n// than stiffness and damping. Bounce stays at zero for product UI.\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  duration: 0.15,\n  bounce: 0,\n} as const;\n\n/** Content swaps, label and icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  duration: 0.2,\n  bounce: 0,\n} as const;\n\n/** Overlay panel entrances, modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  duration: 0.25,\n  bounce: 0,\n} as const;\n\n/** Shared-layout glides, pills and indicators moving between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  duration: 0.22,\n  bounce: 0,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt). */\nexport const SPRING_MOUSE = {\n  stiffness: 320,\n  damping: 26,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders). Critically damped `useSpring` config,\n * so the value follows the pointer closely and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}