{
  "$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> | 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<HTMLInputElement>(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    <div\n      className={cn(\n        \"grid transition-[grid-template-rows] ease-[cubic-bezier(0.16,1,0.3,1)]\",\n        mounted && !dismissed\n          ? \"[grid-template-rows:1fr] duration-500\"\n          : \"[grid-template-rows:0fr] duration-300\",\n      )}\n    >\n      <div className=\"overflow-hidden\">\n        <div\n          className={cn(\n            \"relative flex flex-wrap items-center justify-center gap-x-6 gap-y-3 px-12 py-3\",\n            \"after:pointer-events-none after:absolute after:bottom-0 after:inset-x-0 after:h-px after:bg-gradient-to-r after:from-transparent after:via-border after:to-transparent\",\n            \"transition-[opacity,transform] duration-150\",\n            dismissed && \"opacity-0 -translate-y-1\",\n            className,\n          )}\n        >\n          {/* title */}\n          <span className=\"text-sm font-medium text-foreground/70\">\n            {title}\n          </span>\n\n          {/* ── Morphing Pill ── */}\n          <div\n            className={cn(\n              \"relative h-9 overflow-hidden rounded-full border transition-all duration-500 ease-[cubic-bezier(0.16,1,0.3,1)]\",\n              status === \"idle\" &&\n                !error &&\n                \"w-64 border-border bg-foreground/[0.03] focus-within:border-foreground/20\",\n              status === \"idle\" &&\n                error &&\n                \"w-64 border-destructive/40 bg-foreground/[0.03] animate-[nl-shake_0.4s_ease-out]\",\n              status === \"loading\" &&\n                \"w-40 border-foreground/15 bg-foreground/[0.03]\",\n              status === \"success\" &&\n                \"w-[136px] border-emerald-500/30 bg-emerald-500/[0.06] animate-[nl-land_0.4s_ease-out]\",\n            )}\n          >\n            {/* idle: input + magnetic arrow */}\n            <form\n              onSubmit={handleSubmit}\n              className={cn(\n                \"absolute inset-0 flex items-center transition-opacity duration-200\",\n                status === \"idle\"\n                  ? \"opacity-100\"\n                  : \"pointer-events-none opacity-0\",\n              )}\n            >\n              <input\n                ref={inputRef}\n                type=\"email\"\n                value={email}\n                onChange={(e) => 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              <button\n                type=\"submit\"\n                className={cn(\n                  \"mr-1.5 flex size-6 shrink-0 items-center justify-center rounded-full transition-all duration-300\",\n                  email\n                    ? \"scale-100 bg-foreground text-background\"\n                    : \"scale-75 bg-transparent text-foreground/20\",\n                )}\n              >\n                <ArrowRight className=\"size-3\" />\n              </button>\n            </form>\n\n            {/* loading: spinner + text */}\n            <div\n              className={cn(\n                \"absolute inset-0 flex items-center justify-center gap-2 transition-opacity duration-200\",\n                status === \"loading\"\n                  ? \"opacity-100 delay-150\"\n                  : \"pointer-events-none opacity-0\",\n              )}\n            >\n              <svg\n                className=\"size-3.5 animate-[nl-spin_0.8s_linear_infinite]\"\n                viewBox=\"0 0 16 16\"\n                fill=\"none\"\n              >\n                <circle\n                  cx=\"8\"\n                  cy=\"8\"\n                  r=\"6.5\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"1.5\"\n                  className=\"text-foreground/10\"\n                />\n                <path\n                  d=\"M14.5 8a6.5 6.5 0 0 0-6.5-6.5\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"1.5\"\n                  strokeLinecap=\"round\"\n                  className=\"text-foreground/50\"\n                />\n              </svg>\n              <span className=\"text-xs font-medium text-foreground/40\">\n                Subscribing\n              </span>\n            </div>\n\n            {/* success: drawn checkmark + text */}\n            <div\n              className={cn(\n                \"absolute inset-0 flex items-center justify-center gap-1.5 transition-opacity duration-200\",\n                status === \"success\"\n                  ? \"opacity-100 delay-150\"\n                  : \"pointer-events-none opacity-0\",\n              )}\n            >\n              <svg\n                className=\"size-3.5 text-emerald-500\"\n                viewBox=\"0 0 16 16\"\n                fill=\"none\"\n              >\n                <path\n                  d=\"M3 8.5L6.5 12L13 4\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"1.5\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                  pathLength={1}\n                  strokeDasharray={1}\n                  strokeDashoffset={1}\n                  className={\n                    status === \"success\"\n                      ? \"animate-[nl-check_0.4s_cubic-bezier(0.65,0,0.35,1)_0.2s_forwards]\"\n                      : \"\"\n                  }\n                />\n              </svg>\n              <span className=\"text-xs font-medium text-emerald-600 dark:text-emerald-400\">\n                {\"You're in\"}\n              </span>\n            </div>\n          </div>\n\n          {/* dismiss */}\n          <button\n            type=\"button\"\n            onClick={handleDismiss}\n            className=\"absolute right-3 top-1/2 -translate-y-1/2 rounded-full p-1 text-foreground/20 transition-all duration-150 hover:bg-foreground/5 hover:text-foreground/50 active:scale-90\"\n            aria-label=\"Dismiss\"\n          >\n            <X className=\"size-3.5\" />\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ruixen/banner-newsletter.tsx"
    }
  ]
}