{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "upsell-subscription-only",
  "type": "registry:block",
  "title": "Subscription-only buy box",
  "description": "No one-time option: cadence, next charge date, how to cancel and the guarantee all sit above the button.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "upsell",
    "supplements",
    "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/price.json",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "upsell/upsell-subscription-only.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { CalendarClock, Check, RotateCcw, ShieldCheck, XCircle } from \"lucide-react\"\nimport { cn, money } from \"@/components/storekit/lib/cn\"\nimport { Button } from \"@/components/storekit/ui/button\"\nimport { Price } from \"@/components/storekit/ui/price\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* A subscription-only buy box. There is no one-time option, so the terms\n   do the persuading: price per delivery, cadence, when the next charge\n   lands, how to cancel and the guarantee all sit above the button. The next\n   charge date is computed after mount so server and client markup match. */\n\nconst EASE = [0.23, 1, 0.32, 1] as const\nconst RING = \"outline-none focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper\"\n\nfunction arrows(e: React.KeyboardEvent, n: number, current: number, pick: (i: number) => void) {\n  if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") {\n    e.preventDefault()\n    pick((current + 1) % n)\n  } else if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") {\n    e.preventDefault()\n    pick((current - 1 + n) % n)\n  }\n}\n\nexport function UpsellSubscriptionOnly({\n  title = \"Daily Foundation\",\n  price = 4900,\n  servings = 30,\n  cadences = [30, 60, 90],\n  guaranteeDays = 30,\n  cancelNote = \"Cancel online in two clicks from your account. No calls, no fees.\",\n  onSubscribe,\n  className,\n}: {\n  title?: string\n  /** Price per delivery in integer cents. */\n  price?: number\n  /** Servings per delivery, used for the per-serving line. */\n  servings?: number\n  cadences?: number[]\n  guaranteeDays?: number\n  cancelNote?: string\n  onSubscribe?: (v: { everyDays: number; price: number }) => void\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"upsell\", \"upsell-subscription-only\")\n  const [every, setEvery] = useState(cadences[0] ?? 30)\n  const [nextDate, setNextDate] = useState<string | null>(null)\n  const [status, setStatus] = useState<\"idle\" | \"loading\" | \"added\">(\"idle\")\n  const refs = useRef<(HTMLButtonElement | null)[]>([])\n  const timers = useRef<number[]>([])\n  useEffect(() => () => timers.current.forEach((t) => window.clearTimeout(t)), [])\n\n  useEffect(() => {\n    const d = new Date()\n    d.setDate(d.getDate() + every)\n    setNextDate(d.toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" }))\n  }, [every])\n\n  const choose = (i: number, focus = false) => {\n    if (cadences[i] !== every) sk.emit(\"interact\", `every_${cadences[i]}d`, undefined)\n    setEvery(cadences[i])\n    if (focus) refs.current[i]?.focus()\n  }\n\n  const subscribe = () => {\n    if (status !== \"idle\") return\n    setStatus(\"loading\")\n    sk.emit(\"convert\", \"subscribe\", price)\n    timers.current.push(\n      window.setTimeout(() => {\n        setStatus(\"added\")\n        onSubscribe?.({ everyDays: every, price })\n        timers.current.push(window.setTimeout(() => setStatus(\"idle\"), 1200))\n      }, 700)\n    )\n  }\n\n  const terms = [\n    { icon: CalendarClock, label: \"Next charge\", value: nextDate ? `${nextDate}, then every ${every} days` : `In ${every} days, then every ${every} days` },\n    { icon: XCircle, label: \"Cancel\", value: cancelNote },\n    { icon: RotateCcw, label: \"Change\", value: \"Skip a delivery, swap cadence or pause anytime.\" },\n  ]\n\n  return (\n    <div {...sk.attrs} ref={sk.ref as React.RefObject<HTMLDivElement>} className={cn(\"flex w-full flex-col gap-5 rounded-sk-card border border-sk-line bg-sk-card p-5 sm:p-6\", className)}>\n      <div className=\"flex flex-col gap-2\">\n        <span className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">Subscription</span>\n        <div className=\"flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1\">\n          <span className=\"font-sk-display text-xl font-semibold leading-[1.1] tracking-[-0.02em] text-sk-ink\">{title}</span>\n          <Price amount={price} suffix=\"/ delivery\" per={servings ? { amount: Math.round(price / servings), unit: \"serving\" } : undefined} />\n        </div>\n      </div>\n\n      <div className=\"flex flex-col gap-2\">\n        <span className=\"text-sm font-medium text-sk-ink\">Deliver every</span>\n        <div role=\"radiogroup\" aria-label=\"Delivery cadence\" className=\"grid grid-cols-3 gap-2\">\n          {cadences.map((c, i) => {\n            const on = c === every\n            return (\n              <button\n                key={c}\n                ref={(el) => {\n                  refs.current[i] = el\n                }}\n                type=\"button\"\n                role=\"radio\"\n                aria-checked={on}\n                tabIndex={on ? 0 : -1}\n                onClick={() => choose(i)}\n                onKeyDown={(e) => arrows(e, cadences.length, cadences.indexOf(every), (n) => choose(n, true))}\n                className={cn(\n                  \"h-11 rounded-sk-btn border text-sm font-medium tabular-nums transition-[border-color,background-color,color,transform] duration-200 active:scale-[0.97]\",\n                  on ? \"border-sk-accent bg-sk-accent/[0.08] text-sk-ink\" : \"border-sk-line text-sk-ink hover:border-sk-ink/30\",\n                  RING\n                )}\n              >\n                {c} days\n              </button>\n            )\n          })}\n        </div>\n      </div>\n\n      <dl className=\"flex flex-col divide-y divide-sk-line/70 rounded-sk-card bg-sk-surface px-4\">\n        {terms.map(({ icon: Icon, label, value }) => (\n          <div key={label} className=\"flex items-start gap-3 py-3\">\n            <Icon size={16} className=\"mt-0.5 shrink-0 text-sk-muted\" aria-hidden />\n            <div className=\"flex min-w-0 flex-col gap-0.5 sm:flex-row sm:gap-3\">\n              <dt className=\"w-24 shrink-0 text-sm font-medium text-sk-ink\">{label}</dt>\n              <dd className=\"text-sm leading-[1.5] text-sk-muted tabular-nums\">{value}</dd>\n            </div>\n          </div>\n        ))}\n      </dl>\n\n      <div className=\"flex items-start gap-3 rounded-sk-card border border-sk-savings/30 bg-sk-savings/[0.06] p-4\">\n        <ShieldCheck size={18} className=\"mt-0.5 shrink-0 text-sk-savings\" aria-hidden />\n        <p className=\"text-sm leading-[1.5] text-sk-ink\">\n          <span className=\"font-medium\">{guaranteeDays}-day guarantee.</span> <span className=\"text-sk-muted\">If your first delivery is not right for you, we refund it in full. Keep the product.</span>\n        </p>\n      </div>\n\n      <div className=\"flex flex-col gap-2\">\n        <Button size=\"lg\" className=\"w-full active:scale-[0.97]\" loading={status === \"loading\"} onClick={subscribe}>\n          <AnimatePresence mode=\"wait\" initial={false}>\n            <motion.span\n              key={status === \"added\" ? \"added\" : \"idle\"}\n              initial={{ opacity: 0, y: reduce ? 0 : 6 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: reduce ? 0 : -6 }}\n              transition={{ duration: reduce ? 0 : 0.2, ease: EASE }}\n              className=\"inline-flex items-center gap-2 tabular-nums\"\n            >\n              {status === \"added\" ? (\n                <>\n                  <Check size={16} strokeWidth={2.5} /> Subscription added\n                </>\n              ) : (\n                <>Start subscription · {money(price)}</>\n              )}\n            </motion.span>\n          </AnimatePresence>\n        </Button>\n        <p className=\"text-center text-[12px] text-sk-muted tabular-nums\">\n          Charged {money(price)} today and every {every} days until you cancel.\n        </p>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "components/storekit/upsell/upsell-subscription-only.tsx"
    }
  ],
  "meta": {
    "section": "upsell",
    "variant": "subscription-only",
    "niche": [
      "supplements",
      "any"
    ],
    "commerce": [
      "subscribe",
      "atc"
    ],
    "previewHeight": 620
  }
}