{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "banner-newsletter", "type": "registry:ui", "title": "Banner Newsletter", "description": "A newsletter banner with Inline Morphing — one pill that reshapes across input, loading, and confirmation.", "dependencies": [ "lucide-react" ], "files": [ { "path": "registry/ruixenui/banner-newsletter.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { ArrowRight, X } from \"lucide-react\";\n\n/* ═══════════════════════════════════════════════════════════\n Inline Morphing — a single pill surface that reshapes\n across input → loading → confirmation. No separate button,\n no status message — the pill IS all three states.\n ═══════════════════════════════════════════════════════════ */\n\nexport interface BannerNewsletterProps {\n title?: string;\n onSubscribe?: (email: string) => Promise | void;\n onDismiss?: () => void;\n className?: string;\n}\n\nexport function BannerNewsletter({\n title = \"Stay in the loop\",\n onSubscribe,\n onDismiss,\n className,\n}: BannerNewsletterProps) {\n const [mounted, setMounted] = React.useState(false);\n const [dismissed, setDismissed] = React.useState(false);\n const [email, setEmail] = React.useState(\"\");\n const [status, setStatus] = React.useState<\"idle\" | \"loading\" | \"success\">(\n \"idle\",\n );\n const [error, setError] = React.useState(false);\n const inputRef = React.useRef(null);\n const onDismissRef = React.useRef(onDismiss);\n onDismissRef.current = onDismiss;\n\n /* entrance expand */\n React.useEffect(() => {\n requestAnimationFrame(() => {\n requestAnimationFrame(() => setMounted(true));\n });\n }, []);\n\n const handleDismiss = React.useCallback(() => {\n setDismissed(true);\n setTimeout(() => onDismissRef.current?.(), 350);\n }, []);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!email || status !== \"idle\") return;\n\n setStatus(\"loading\");\n try {\n await onSubscribe?.(email);\n setStatus(\"success\");\n setTimeout(handleDismiss, 2000);\n } catch {\n setStatus(\"idle\");\n setError(true);\n inputRef.current?.focus();\n setTimeout(() => setError(false), 600);\n }\n };\n\n return (\n \n
\n \n {/* title */}\n \n {title}\n \n\n {/* ── Morphing Pill ── */}\n \n {/* idle: input + magnetic arrow */}\n \n setEmail(e.target.value)}\n placeholder=\"you@email.com\"\n className=\"h-full flex-1 bg-transparent pl-4 pr-1 text-sm text-foreground outline-none placeholder:text-muted-foreground/40\"\n required\n />\n \n \n \n \n\n {/* loading: spinner + text */}\n \n \n \n \n \n \n Subscribing\n \n
\n\n {/* success: drawn checkmark + text */}\n \n \n \n \n \n {\"You're in\"}\n \n \n \n\n {/* dismiss */}\n \n \n \n \n \n \n );\n}\n", "type": "registry:ui", "target": "components/ruixen/banner-newsletter.tsx" } ] }