{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "ai-chat-input", "type": "registry:ui", "title": "AI Chat Input", "description": "Vanishing chat input with rotating placeholders, pixel-scatter submit, and spring-timed interactions.", "dependencies": [ "motion" ], "files": [ { "path": "registry/ruixenui/ai-chat-input.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface AiChatInputProps {\n /** Rotating placeholder strings */\n placeholders?: string[];\n /** Callback with the submitted value */\n onSubmit?: (value: string) => void;\n /** Callback fired on every keystroke */\n onChange?: (e: React.ChangeEvent) => void;\n /** Additional CSS classes */\n className?: string;\n}\n\nexport default function AiChatInput({\n placeholders = [\n \"Ask me anything...\",\n \"Describe what you need built\",\n \"Summarize this document for me\",\n \"Write a function that...\",\n \"Explain how this works\",\n ],\n onSubmit,\n onChange,\n className,\n}: AiChatInputProps) {\n const canvasRef = React.useRef(null);\n const inputRef = React.useRef(null);\n const newDataRef = React.useRef<\n Array<{\n x: number;\n y: number;\n r: number;\n color: string;\n vx: number;\n vy: number;\n }>\n >([]);\n const [value, setValue] = React.useState(\"\");\n const [animating, setAnimating] = React.useState(false);\n const [placeholderIndex, setPlaceholderIndex] = React.useState(0);\n\n /* ---- rotating placeholders ---- */\n React.useEffect(() => {\n const id = setInterval(() => {\n setPlaceholderIndex((i) => (i + 1) % placeholders.length);\n }, 3000);\n return () => clearInterval(id);\n }, [placeholders.length]);\n\n /* ---- canvas setup ---- */\n const startAnimation = React.useCallback(() => {\n const cvs = canvasRef.current;\n const input = inputRef.current;\n if (!cvs || !input) return;\n\n cvs.width = 800;\n cvs.height = 800;\n\n const ctx = cvs.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) return;\n\n ctx.clearRect(0, 0, 800, 800);\n\n const style = getComputedStyle(input);\n const fontSize = parseFloat(style.getPropertyValue(\"font-size\"));\n\n ctx.font = `${fontSize * 2}px ${style.fontFamily}`;\n ctx.fillStyle = style.color;\n ctx.fillText(value, 0, fontSize * 2);\n\n const imageData = ctx.getImageData(0, 0, 800, 800);\n const pixelData = imageData.data;\n const pixels: Array<{\n x: number;\n y: number;\n r: number;\n color: string;\n vx: number;\n vy: number;\n }> = [];\n\n for (let y = 0; y < 800; y += 2) {\n for (let x = 0; x < 800; x++) {\n const idx = (y * 800 + x) * 4;\n if (pixelData[idx + 3] > 128) {\n const r = pixelData[idx];\n const g = pixelData[idx + 1];\n const b = pixelData[idx + 2];\n pixels.push({\n x: x / 2,\n y: y / 2,\n r: 1,\n color: `rgba(${r}, ${g}, ${b}, ${pixelData[idx + 3] / 255})`,\n vx: (Math.random() - 0.5) * 1.5,\n vy: -(1.5 + Math.random() * 1.5),\n });\n }\n }\n }\n\n newDataRef.current = pixels;\n setAnimating(true);\n }, [value]);\n\n /* ---- vanish animation loop ---- */\n React.useEffect(() => {\n if (!animating) return;\n const cvs = canvasRef.current;\n if (!cvs) return;\n const ctx = cvs.getContext(\"2d\");\n if (!ctx) return;\n\n let frame: number;\n\n const draw = () => {\n ctx.clearRect(0, 0, cvs.width, cvs.height);\n const remaining = newDataRef.current;\n let alive = 0;\n\n for (let i = remaining.length - 1; i >= 0; i--) {\n const p = remaining[i];\n if (p.x < 0 || p.r <= 0.1) continue;\n alive++;\n ctx.beginPath();\n ctx.rect(p.x, p.y, p.r, p.r);\n ctx.fillStyle = p.color;\n ctx.strokeStyle = p.color;\n ctx.stroke();\n\n p.x += p.vx;\n p.y += p.vy;\n p.vy *= 0.985;\n p.r *= 0.97;\n }\n\n if (alive > 0) {\n frame = requestAnimationFrame(draw);\n } else {\n setAnimating(false);\n setValue(\"\");\n }\n };\n\n frame = requestAnimationFrame(draw);\n return () => cancelAnimationFrame(frame);\n }, [animating]);\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !animating) {\n vanishAndSubmit();\n }\n };\n\n const vanishAndSubmit = () => {\n if (!value.trim()) return;\n startAnimation();\n onSubmit?.(value);\n };\n\n const handleChange = (e: React.ChangeEvent) => {\n if (!animating) {\n setValue(e.target.value);\n onChange?.(e);\n }\n };\n\n return (\n \n \n\n \n\n {/* animated placeholder */}\n
\n \n {!value && (\n \n {placeholders[placeholderIndex]}\n \n )}\n \n
\n\n {/* submit button */}\n \n \n \n \n \n \n \n \n \n );\n}\n", "type": "registry:ui", "target": "components/ruixen/ai-chat-input.tsx" } ] }