{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cta-band",
  "type": "registry:block",
  "title": "CTA band",
  "description": "Full-bleed ink closing band with a display headline, inline email capture and a drifting product tile.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "marketing",
    "any"
  ],
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://storekit-eight.vercel.app/r/cn.json",
    "https://storekit-eight.vercel.app/r/button.json",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "marketing/cta-band.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { ArrowRight, Check } from \"lucide-react\"\nimport { cn } from \"@/components/storekit/lib/cn\"\nimport { Button } from \"@/components/storekit/ui/button\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Closing band in inverted ink. One display headline, an inline email\n   capture with a success state, and a tilted product photo tile at the right\n   edge that rises in once when the band enters view. */\n\nexport function CtaBand({\n  title = \"New flavors, first access.\",\n  body = \"One email when something new drops. Nothing else.\",\n  image = \"/brands/morrow/greens.webp\",\n  imageAlt = \"\",\n  cta = \"Notify me\",\n  onSubmit,\n  className,\n}: {\n  title?: string\n  body?: string\n  image?: string\n  imageAlt?: string\n  cta?: string\n  onSubmit?: (email: string) => void | Promise<void>\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"marketing\", \"cta-band\")\n  const [email, setEmail] = React.useState(\"\")\n  const [state, setState] = React.useState<\"idle\" | \"loading\" | \"done\" | \"error\">(\"idle\")\n  const inputId = React.useId()\n\n  const submit = async (e: React.FormEvent) => {\n    e.preventDefault()\n    if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email.trim())) return setState(\"error\")\n    setState(\"loading\")\n    await Promise.all([onSubmit?.(email.trim()), new Promise((r) => setTimeout(r, 700))])\n    setState(\"done\")\n    sk.emit(\"convert\", \"email_submit\")\n  }\n\n  return (\n    <section ref={sk.ref as React.Ref<HTMLElement>} {...sk.attrs} className={cn(\"relative w-full overflow-hidden bg-sk-ink text-sk-paper\", className)}>\n      <div className=\"relative mx-auto max-w-6xl px-4 py-16 sm:px-8 sm:py-24\">\n        <div className=\"max-w-xl sm:max-w-[62%] lg:max-w-xl\">\n          <h2 className=\"font-sk-display text-3xl font-semibold leading-[1.1] tracking-[-0.02em] sm:text-4xl\">{title}</h2>\n          <p className=\"mt-3 text-[15px] leading-[1.5] text-sk-paper/70\">{body}</p>\n\n          <div className=\"mt-8 min-h-[72px]\">\n            <AnimatePresence mode=\"wait\" initial={false}>\n              {state === \"done\" ? (\n                <motion.p\n                  key=\"done\"\n                  role=\"status\"\n                  initial={{ opacity: 0, y: reduce ? 0 : 8 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}\n                  className=\"flex h-12 items-center gap-3 text-[15px]\"\n                >\n                  <span className=\"grid h-7 w-7 place-items-center rounded-full bg-sk-accent text-sk-accent-ink\">\n                    <Check size={14} strokeWidth={2.5} />\n                  </span>\n                  You are on the list. Watch for the next drop.\n                </motion.p>\n              ) : (\n                <motion.form\n                  key=\"form\"\n                  noValidate\n                  onSubmit={submit}\n                  exit={{ opacity: 0, y: reduce ? 0 : -8 }}\n                  transition={{ duration: 0.2 }}\n                  className=\"flex max-w-md flex-col gap-2 sm:flex-row\"\n                >\n                  <label htmlFor={inputId} className=\"sr-only\">Email</label>\n                  <input\n                    id={inputId}\n                    type=\"email\"\n                    autoComplete=\"email\"\n                    placeholder=\"you@lab.edu\"\n                    value={email}\n                    aria-invalid={state === \"error\" || undefined}\n                    aria-describedby={state === \"error\" ? `${inputId}-err` : undefined}\n                    onChange={(e) => {\n                      setEmail(e.target.value)\n                      if (state === \"error\") setState(\"idle\")\n                    }}\n                    className={cn(\n                      \"h-12 w-full min-w-0 shrink-0 rounded-sk-btn border sm:w-auto sm:flex-1 sm:shrink bg-sk-paper/[0.06] px-4 text-sm text-sk-paper outline-none transition-[border-color,box-shadow] duration-200 placeholder:text-sk-paper/45\",\n                      \"focus:border-sk-paper/60 focus:ring-[3px] focus:ring-sk-paper/15\",\n                      state === \"error\" ? \"border-sk-sale\" : \"border-sk-paper/20\"\n                    )}\n                  />\n                  <Button type=\"submit\" size=\"lg\" loading={state === \"loading\"} className=\"focus-visible:ring-offset-sk-ink\">\n                    {cta}\n                    <ArrowRight size={16} />\n                  </Button>\n                </motion.form>\n              )}\n            </AnimatePresence>\n            {state === \"error\" ? (\n              <p id={`${inputId}-err`} className=\"mt-2 text-xs text-sk-paper/80\">Enter a valid email.</p>\n            ) : null}\n          </div>\n        </div>\n        <div\n          aria-hidden={imageAlt ? undefined : true}\n          className=\"pointer-events-none absolute -right-8 top-1/2 hidden w-[min(260px,30%)] -translate-y-1/2 sm:block lg:right-8\"\n        >\n          <motion.img\n            src={image}\n            alt={imageAlt}\n            className=\"aspect-square w-full rotate-[4deg] rounded-sk-card border border-sk-paper/10 object-cover\"\n            initial={reduce ? false : { opacity: 0, y: 8 }}\n            whileInView={{ opacity: 1, y: 0 }}\n            viewport={{ once: true, amount: 0.4 }}\n            transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}\n          />\n        </div>\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:block",
      "target": "components/storekit/marketing/cta-band.tsx"
    }
  ],
  "meta": {
    "section": "marketing",
    "variant": "band",
    "niche": [
      "any"
    ],
    "commerce": [
      "email-capture",
      "cta"
    ],
    "previewHeight": 380
  }
}