{
  "$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<ChatMessage[]>([]);\n  const idRef = React.useRef(0);\n  const timersRef = React.useRef<ReturnType<typeof setTimeout>[]>([]);\n  const audioRef = React.useRef<AudioContext | null>(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    <div className={cn(\"relative mx-auto w-full max-w-lg\", className)}>\n      {/* the input card */}\n      <div className=\"relative rounded-3xl border border-border bg-background p-1 shadow-[0_10px_20px_-6px_rgba(0,0,0,0.1)]\">\n        <div className=\"relative z-[2] flex items-center justify-between gap-2 rounded-3xl bg-background p-1.5\">\n          <div className=\"flex flex-1 items-center gap-3 pr-1\">\n            <Button\n              type=\"button\"\n              variant=\"secondary\"\n              size=\"icon\"\n              aria-label=\"Add attachment\"\n              className=\"size-10 shrink-0 rounded-xl\"\n            >\n              <Plus className=\"size-5\" />\n            </Button>\n            <Input\n              value={value}\n              onChange={(e) => 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          </div>\n          <Button\n            type=\"button\"\n            onClick={handleSend}\n            onMouseDown={(e) => 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            <Send className=\"size-5\" strokeWidth={2.25} />\n          </Button>\n        </div>\n\n        {/* bubble stack — floats above the card */}\n        <div className=\"pointer-events-none absolute bottom-[70px] right-0 z-[1] flex w-full flex-col items-end gap-2\">\n          <AnimatePresence initial={false}>\n            {visible.map((m) => (\n              <motion.div\n                key={m.id}\n                layout\n                initial={{ opacity: 0, y: 24, scale: 0.85 }}\n                animate={{ opacity: 1, y: 0, scale: 1 }}\n                exit={{ opacity: 0, scale: 0.85 }}\n                transition={{ type: \"spring\", stiffness: 420, damping: 32 }}\n                className={cn(\n                  \"max-w-[260px] break-words px-3.5 py-2.5 text-sm shadow-[0_10px_20px_-6px_rgba(0,0,0,0.15)]\",\n                  m.sender === \"user\"\n                    ? \"self-end rounded-[14px_14px_6px_14px] border border-border bg-background text-foreground\"\n                    : \"self-start rounded-[14px_14px_14px_6px] bg-primary text-primary-foreground\",\n                )}\n              >\n                {m.text}\n              </motion.div>\n            ))}\n          </AnimatePresence>\n        </div>\n      </div>\n\n      {/* spectrum glow — reveals from below once a conversation starts */}\n      <motion.div\n        aria-hidden\n        initial={false}\n        animate={hasMessages ? { opacity: 1, y: 0 } : { opacity: 0, y: \"30%\" }}\n        transition={{ duration: 0.8, ease: \"easeOut\" }}\n        className=\"absolute left-1/2 top-0 -z-[1] flex h-[100vh] w-full max-w-3xl -translate-x-1/2\"\n      >\n        {[0, 1, 2].map((col) => (\n          <div\n            key={col}\n            className={cn(\n              \"flex h-full w-full flex-col items-stretch -space-y-3\",\n              col === 1 && \"-translate-y-20\",\n            )}\n          >\n            {gradientColors.map((color, i) => (\n              <div\n                key={i}\n                className=\"w-full flex-1 blur-xl\"\n                style={{ backgroundColor: color }}\n              />\n            ))}\n          </div>\n        ))}\n      </motion.div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ruixen/gradient-chat-input.tsx"
    }
  ]
}