{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "gradient-chat-input", "type": "registry:ui", "title": "Gradient Chat Input", "description": "Minimal chat input that streams animated message bubbles, plays synthesized send/receive sounds, and reveals a blurred spectrum glow once a conversation starts.", "dependencies": [ "lucide-react", "motion" ], "registryDependencies": [ "button", "input" ], "files": [ { "path": "registry/ruixenui/gradient-chat-input.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Plus, Send } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\n\n/* ------------------------------------------------------------------ */\n/* types */\n/* ------------------------------------------------------------------ */\nexport interface ChatMessage {\n id: number;\n text: string;\n sender: \"user\" | \"bot\";\n}\n\nexport interface GradientChatInputProps {\n /** Placeholder shown inside the text field. */\n placeholder?: string;\n /** Auto-reply pushed back after a user message. Pass `null` to disable. */\n autoReply?: string | null;\n /** Delay (ms) before the auto-reply lands. */\n autoReplyDelay?: number;\n /** Max number of bubbles kept on screen. */\n maxVisible?: number;\n /** Play synthesized send / receive sounds. */\n sound?: boolean;\n /** The spectrum used for the reveal glow (top → bottom). */\n gradientColors?: string[];\n /** Fired whenever the user submits a message. */\n onSend?: (message: string) => void;\n className?: string;\n}\n\n/* ------------------------------------------------------------------ */\n/* defaults */\n/* ------------------------------------------------------------------ */\nconst DEFAULT_GRADIENT = [\n \"#FC2BA3\",\n \"#FC6D35\",\n \"#F9C83D\",\n \"#C2D6E1\",\n \"#144EC5\",\n];\n\n/* ------------------------------------------------------------------ */\n/* component */\n/* ------------------------------------------------------------------ */\nexport default function GradientChatInput({\n placeholder = \"Send Message\",\n autoReply = \"Got it — looking into that now ✨\",\n autoReplyDelay = 650,\n maxVisible = 4,\n sound = true,\n gradientColors = DEFAULT_GRADIENT,\n onSend,\n className,\n}: GradientChatInputProps) {\n const [value, setValue] = React.useState(\"\");\n const [messages, setMessages] = React.useState([]);\n const idRef = React.useRef(0);\n const timersRef = React.useRef[]>([]);\n const audioRef = React.useRef(null);\n\n /* lazy AudioContext — only created on the first user gesture */\n const getAudioContext = React.useCallback(() => {\n if (typeof window === \"undefined\") return null;\n if (!audioRef.current) {\n const Ctx =\n window.AudioContext ||\n (window as typeof window & { webkitAudioContext?: typeof AudioContext })\n .webkitAudioContext;\n if (Ctx) audioRef.current = new Ctx();\n }\n return audioRef.current;\n }, []);\n\n /* two-note blip synthesized inline — no audio assets to ship */\n const playChime = React.useCallback(\n (notes: { freq: number; at: number }[], volume: number) => {\n if (!sound) return;\n const ctx = getAudioContext();\n if (!ctx) return;\n if (ctx.state === \"suspended\") void ctx.resume();\n\n notes.forEach(({ freq, at }) => {\n const osc = ctx.createOscillator();\n const gain = ctx.createGain();\n const start = ctx.currentTime + at;\n osc.type = \"sine\";\n osc.frequency.value = freq;\n gain.gain.setValueAtTime(0.0001, start);\n gain.gain.exponentialRampToValueAtTime(volume, start + 0.012);\n gain.gain.exponentialRampToValueAtTime(0.0001, start + 0.18);\n osc.connect(gain).connect(ctx.destination);\n osc.start(start);\n osc.stop(start + 0.2);\n });\n },\n [sound, getAudioContext],\n );\n\n const playSend = React.useCallback(\n () =>\n playChime(\n [\n { freq: 523.25, at: 0 },\n { freq: 783.99, at: 0.06 },\n ],\n 0.05,\n ),\n [playChime],\n );\n\n const playReceive = React.useCallback(\n () =>\n playChime(\n [\n { freq: 392.0, at: 0 },\n { freq: 587.33, at: 0.08 },\n ],\n 0.05,\n ),\n [playChime],\n );\n\n /* cleanup pending timers + audio context on unmount */\n React.useEffect(() => {\n const timers = timersRef.current;\n return () => {\n timers.forEach(clearTimeout);\n void audioRef.current?.close();\n };\n }, []);\n\n const pushMessage = (text: string, sender: ChatMessage[\"sender\"]) =>\n setMessages((prev) => [...prev, { id: idRef.current++, text, sender }]);\n\n const handleSend = () => {\n const text = value.trim();\n if (!text) return;\n\n onSend?.(text);\n pushMessage(text, \"user\");\n playSend();\n setValue(\"\");\n\n if (autoReply) {\n const t = setTimeout(() => {\n pushMessage(autoReply, \"bot\");\n playReceive();\n timersRef.current = timersRef.current.filter((timer) => timer !== t);\n }, autoReplyDelay);\n timersRef.current.push(t);\n }\n };\n\n const hasText = value.trim().length > 0;\n const hasMessages = messages.length > 0;\n const visible = messages.slice(-maxVisible);\n\n return (\n
\n {/* the input card */}\n
\n
\n
\n \n \n \n setValue(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n handleSend();\n }\n }}\n placeholder={placeholder}\n aria-label=\"Message\"\n className=\"h-auto flex-1 border-0 bg-transparent px-0 py-0 text-base shadow-none focus-visible:ring-0 dark:bg-transparent md:text-sm\"\n />\n
\n e.preventDefault()}\n variant={hasText ? \"default\" : \"secondary\"}\n size=\"icon\"\n aria-label=\"Send message\"\n className=\"size-10 shrink-0 rounded-xl transition-colors active:scale-95\"\n >\n \n \n
\n\n {/* bubble stack — floats above the card */}\n
\n \n {visible.map((m) => (\n \n {m.text}\n \n ))}\n \n
\n
\n\n {/* spectrum glow — reveals from below once a conversation starts */}\n \n {[0, 1, 2].map((col) => (\n \n {gradientColors.map((color, i) => (\n \n ))}\n
\n ))}\n \n \n );\n}\n", "type": "registry:ui", "target": "components/ruixen/gradient-chat-input.tsx" } ] }