{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hero-drop",
  "type": "registry:block",
  "title": "Drop launch hero",
  "description": "Coming soon teaser with the real release date, compact countdown and notify-me email form that flips to available now after release.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "hero",
    "apparel",
    "beverage",
    "any"
  ],
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://storekit-eight.vercel.app/r/cn.json",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "hero/hero-drop.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useId, useMemo, useState } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { ArrowRight, Bell, Check } from \"lucide-react\"\nimport { cn } from \"@/components/storekit/lib/cn\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Drop launch or coming soon hero. A teaser image, the real release date\n   and a notify-me form. Before `releaseAt` the hero shows a compact\n   countdown and the email form; after it, the same block flips to an\n   \"available now\" state with a shop CTA, so it never shows a dead timer.\n   The form validates natively, hands the address to `onSubscribe` (which\n   may be async), and confirms in place. The visual countdown is hidden from\n   screen readers; the release date is a real <time> element. Stacks image\n   first on mobile. */\n\nconst EASE = [0.23, 1, 0.32, 1] as const\n\nfunction parts(ms: number) {\n  const s = Math.max(0, Math.floor(ms / 1000))\n  return [\n    [Math.floor(s / 86400), \"d\"],\n    [Math.floor((s % 86400) / 3600), \"h\"],\n    [Math.floor((s % 3600) / 60), \"m\"],\n    [s % 60, \"s\"],\n  ] as const\n}\n\nexport function HeroDrop({\n  releaseAt,\n  eyebrow = \"Drop 04\",\n  headline = \"Sparkling Recovery, blood orange.\",\n  subcopy = \"A limited run of 4,000 cases. Subscribers get first access an hour before everyone else.\",\n  image = \"/placeholder.webp\",\n  imageAlt = \"\",\n  teaser = true,\n  placeholder = \"you@email.com\",\n  buttonLabel = \"Notify me\",\n  success = \"You're on the list. We'll email you the minute it drops.\",\n  consent = \"One email when it launches. No spam, unsubscribe anytime.\",\n  liveCta = { label: \"Shop the drop\", href: \"#\" },\n  onSubscribe,\n  className,\n}: {\n  /** Real release time. ISO string, epoch ms or Date. */\n  releaseAt: string | number | Date\n  eyebrow?: string\n  headline?: string\n  subcopy?: string\n  image?: string\n  imageAlt?: string\n  /** Darken and soften the image until release, and lift the veil on hover. */\n  teaser?: boolean\n  placeholder?: string\n  buttonLabel?: string\n  success?: string\n  consent?: string\n  liveCta?: { label: string; href: string }\n  onSubscribe?: (email: string) => void | Promise<void>\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"hero\", \"hero-drop\")\n  const id = useId()\n  const end = useMemo(() => new Date(releaseAt).getTime(), [releaseAt])\n  const [now, setNow] = useState<number | null>(null)\n  const [email, setEmail] = useState(\"\")\n  const [state, setState] = useState<\"idle\" | \"busy\" | \"done\" | \"error\">(\"idle\")\n\n  useEffect(() => {\n    setNow(Date.now())\n    const t = setInterval(() => setNow(Date.now()), 1000)\n    return () => clearInterval(t)\n  }, [])\n\n  const live = now !== null && Number.isFinite(end) && now >= end\n  const dateText = useMemo(\n    () => (now !== null && Number.isFinite(end) ? new Intl.DateTimeFormat(undefined, { weekday: \"long\", month: \"long\", day: \"numeric\", hour: \"numeric\", minute: \"2-digit\" }).format(end) : \"\"),\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [end, now === null]\n  )\n\n  const submit = async (e: React.FormEvent) => {\n    e.preventDefault()\n    if (state === \"busy\") return\n    setState(\"busy\")\n    try {\n      await onSubscribe?.(email)\n      setState(\"done\")\n      sk.emit(\"convert\", \"notify_signup\")\n    } catch {\n      setState(\"error\")\n    }\n  }\n\n  return (\n    <section ref={sk.ref} {...sk.attrs} className={cn(\"bg-sk-paper text-sk-ink\", className)}>\n      <div className=\"mx-auto grid w-full max-w-6xl items-center gap-8 px-4 py-10 sm:px-8 sm:py-14 lg:grid-cols-2 lg:gap-16\">\n        <motion.div\n          initial={{ opacity: 0, scale: reduce ? 1 : 0.97 }}\n          animate={{ opacity: 1, scale: 1 }}\n          transition={{ duration: 0.7, ease: EASE }}\n          className=\"group relative aspect-square overflow-hidden rounded-sk-card border border-sk-line bg-sk-surface lg:aspect-[4/5]\"\n        >\n          <img\n            src={image}\n            alt={imageAlt}\n            className={cn(\n              \"absolute inset-0 h-full w-full object-cover transition-[filter,transform] duration-[1200ms] ease-sk-out\",\n              teaser && !live && \"scale-[1.04] brightness-[0.55] saturate-[0.6] group-hover:scale-100 group-hover:brightness-[0.8] group-hover:saturate-100\"\n            )}\n          />\n          <span className=\"absolute left-4 top-4 inline-flex items-center gap-2 rounded-sk-chip bg-[rgb(0_0_0/0.55)] px-3 py-1.5 font-sk-mono text-[11px] uppercase tracking-[0.14em] text-[rgb(255_255_255)] backdrop-blur-sm\">\n            <span className=\"relative flex h-1.5 w-1.5\">\n              <span className=\"absolute inline-flex h-full w-full animate-ping rounded-full bg-[rgb(255_255_255)] opacity-60 motion-reduce:hidden\" />\n              <span className=\"relative inline-flex h-1.5 w-1.5 rounded-full bg-[rgb(255_255_255)]\" />\n            </span>\n            {live ? \"Available now\" : \"Coming soon\"}\n          </span>\n          {!live && now !== null ? (\n            <div aria-hidden className=\"absolute inset-x-4 bottom-4 flex justify-center gap-2 font-sk-mono text-[rgb(255_255_255)] sm:inset-x-6 sm:bottom-6\">\n              {parts(end - now).map(([v, u]) => (\n                <span key={u} className=\"flex min-w-[64px] items-baseline justify-center gap-0.5 rounded-sk-card bg-[rgb(0_0_0/0.45)] px-3 py-2.5 backdrop-blur-md\">\n                  <span className=\"text-[24px] font-medium tabular-nums sm:text-[30px]\">{String(v).padStart(2, \"0\")}</span>\n                  <span className=\"text-[11px] uppercase text-[rgb(255_255_255/0.7)]\">{u}</span>\n                </span>\n              ))}\n            </div>\n          ) : null}\n        </motion.div>\n\n        <motion.div initial={{ opacity: 0, y: reduce ? 0 : 8 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, ease: EASE, delay: reduce ? 0 : 0.1 }}>\n          <p className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">{eyebrow}</p>\n          <h1 className=\"mt-4 text-balance font-sk-display font-semibold leading-[1.06] tracking-[-0.02em]\" style={{ fontSize: \"clamp(2.1rem, 4.4vw, 3.5rem)\" }}>\n            {headline}\n          </h1>\n          <p className=\"mt-4 max-w-md text-[15px] leading-[1.55] text-sk-muted\">{subcopy}</p>\n          {Number.isFinite(end) ? (\n            <p className=\"mt-6 flex items-center gap-2 text-sm\">\n              <span className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">{live ? \"Released\" : \"Drops\"}</span>\n              <time dateTime={now !== null ? new Date(end).toISOString() : undefined} className=\"font-medium\">\n                {dateText || \" \"}\n              </time>\n            </p>\n          ) : null}\n\n          <div className=\"mt-6 min-h-[120px]\">\n            {live ? (\n              <a\n                href={liveCta.href}\n                onClick={() => sk.emit(\"interact\", \"cta\")}\n                className=\"sk-btn group inline-flex min-h-12 items-center justify-center gap-2 py-2.5 text-center leading-tight rounded-sk-btn bg-sk-accent px-7 text-[15px] font-medium text-sk-accent-ink outline-none transition-[filter,transform] duration-200 ease-sk-out hover:brightness-[1.06] focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper active:scale-[0.97]\"\n              >\n                {liveCta.label}\n                <ArrowRight size={16} aria-hidden className=\"transition-transform duration-300 ease-sk-out group-hover:translate-x-0.5\" />\n              </a>\n            ) : (\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 : 6 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    exit={{ opacity: 0 }}\n                    transition={{ duration: 0.3, ease: EASE }}\n                    className=\"flex max-w-md items-start gap-3 rounded-sk-card border border-sk-line bg-sk-surface p-4 text-[15px]\"\n                  >\n                    <span className=\"grid h-6 w-6 shrink-0 place-items-center rounded-full bg-sk-savings text-sk-paper\">\n                      <Check size={14} aria-hidden />\n                    </span>\n                    {success}\n                  </motion.p>\n                ) : (\n                  <motion.form key=\"form\" onSubmit={submit} exit={{ opacity: 0, y: reduce ? 0 : -6 }} transition={{ duration: 0.2 }} className=\"max-w-md\">\n                    <label htmlFor={`${id}-email`} className=\"text-sm font-medium\">\n                      Get notified when it drops\n                    </label>\n                    <div className=\"mt-2 flex flex-col gap-2 sm:flex-row\">\n                      <input\n                        id={`${id}-email`}\n                        type=\"email\"\n                        required\n                        autoComplete=\"email\"\n                        inputMode=\"email\"\n                        value={email}\n                        onChange={(e) => setEmail(e.target.value)}\n                        placeholder={placeholder}\n                        aria-describedby={`${id}-consent`}\n                        aria-invalid={state === \"error\" || undefined}\n                        className=\"h-12 w-full min-w-0 rounded-sk-btn border border-sk-line bg-sk-card px-4 text-base sm:flex-1 sm:text-[15px] text-sk-ink outline-none transition-[border-color,box-shadow] placeholder:text-sk-muted/70 focus:border-sk-ink/40 focus-visible:ring-2 focus-visible:ring-sk-accent/40\"\n                      />\n                      <button\n                        type=\"submit\"\n                        disabled={state === \"busy\"}\n                        className=\"sk-btn inline-flex min-h-12 items-center justify-center py-2.5 text-center leading-tight gap-2 rounded-sk-btn bg-sk-accent px-6 text-[15px] font-medium text-sk-accent-ink outline-none transition-[filter,transform,opacity] duration-200 ease-sk-out hover:brightness-[1.06] focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper active:scale-[0.97] disabled:opacity-60\"\n                      >\n                        <Bell size={15} aria-hidden />\n                        {state === \"busy\" ? \"Saving\" : buttonLabel}\n                      </button>\n                    </div>\n                    <p id={`${id}-consent`} className={cn(\"mt-2 text-[12px]\", state === \"error\" ? \"text-sk-sale\" : \"text-sk-muted\")} role={state === \"error\" ? \"alert\" : undefined}>\n                      {state === \"error\" ? \"That didn't go through. Please try again.\" : consent}\n                    </p>\n                  </motion.form>\n                )}\n              </AnimatePresence>\n            )}\n          </div>\n        </motion.div>\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:block",
      "target": "components/storekit/hero/hero-drop.tsx"
    }
  ],
  "docs": "`releaseAt` is required. `onSubscribe` may return a promise; a rejection shows the retry message.",
  "meta": {
    "section": "hero",
    "variant": "drop",
    "niche": [
      "apparel",
      "beverage",
      "any"
    ],
    "commerce": [
      "cta",
      "capture"
    ],
    "previewHeight": 620
  }
}