{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "upsell-subscribe",
  "type": "registry:component",
  "title": "Subscribe and save",
  "description": "One-time vs subscribe radio cards, frequency chips and a benefits list that opens on subscribe.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "upsell",
    "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": "upsell/upsell-subscribe.tsx",
      "content": "\"use client\"\n\nimport { useRef, useState } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { CalendarClock, PackageCheck, Percent, Truck } from \"lucide-react\"\nimport { cn, money } from \"@/components/storekit/lib/cn\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Standalone subscribe and save selector. Two radio cards, delivery\n   frequency chips, and a benefits list that opens when subscribe is picked.\n   Always shows the per-delivery price. */\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\nconst DEFAULT_BENEFITS = [\n  { icon: Percent, label: \"15% off every delivery\" },\n  { icon: Truck, label: \"Free shipping on every order\" },\n  { icon: CalendarClock, label: \"Skip, swap or pause from your account\" },\n  { icon: PackageCheck, label: \"Fresh batch each shipment, third-party tested\" },\n]\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 UpsellSubscribe({\n  price = 5900,\n  discount = 0.15,\n  frequencies = [30, 60, 90],\n  benefits = DEFAULT_BENEFITS,\n  defaultSubscribe = true,\n  onChange,\n  className,\n}: {\n  price?: number\n  discount?: number\n  frequencies?: number[]\n  benefits?: { icon: React.ComponentType<{ size?: number; className?: string }>; label: string }[]\n  defaultSubscribe?: boolean\n  onChange?: (v: { subscribe: boolean; everyDays: number; price: number }) => void\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"upsell\", \"upsell-subscribe\")\n  const [subscribe, setSubscribe] = useState(defaultSubscribe)\n  const [every, setEvery] = useState(frequencies[0] ?? 30)\n  const planRefs = useRef<(HTMLButtonElement | null)[]>([])\n  const freqRefs = useRef<(HTMLButtonElement | null)[]>([])\n  const subPrice = Math.round(price * (1 - discount))\n  const pct = Math.round(discount * 100)\n  const t = { duration: reduce ? 0 : 0.35, ease: EASE }\n\n  const setPlan = (sub: boolean, focus = false) => {\n    if (sub !== subscribe) sk.emit(\"interact\", sub ? \"subscribe\" : \"one_time\", sub ? subPrice : price)\n    setSubscribe(sub)\n    onChange?.({ subscribe: sub, everyDays: every, price: sub ? subPrice : price })\n    if (focus) planRefs.current[sub ? 1 : 0]?.focus()\n  }\n  const setFreq = (i: number, focus = false) => {\n    if (frequencies[i] !== every) sk.emit(\"interact\", `every_${frequencies[i]}d`, undefined)\n    setEvery(frequencies[i])\n    onChange?.({ subscribe, everyDays: frequencies[i], price: subscribe ? subPrice : price })\n    if (focus) freqRefs.current[i]?.focus()\n  }\n\n  const plans = [\n    { sub: false, title: \"One-time purchase\", note: \"Ships once\", amount: price },\n    { sub: true, title: `Subscribe and save ${pct}%`, note: `${money(subPrice)} per delivery`, amount: subPrice },\n  ]\n\n  return (\n    <div {...sk.attrs} ref={sk.ref as React.RefObject<HTMLDivElement>} className={cn(\"flex w-full flex-col gap-3\", className)}>\n      <div role=\"radiogroup\" aria-label=\"Purchase option\" className=\"grid gap-2.5\">\n        {plans.map((p, i) => {\n          const on = subscribe === p.sub\n          return (\n            <button\n              key={p.title}\n              ref={(el) => {\n                planRefs.current[i] = el\n              }}\n              type=\"button\"\n              role=\"radio\"\n              aria-checked={on}\n              tabIndex={on ? 0 : -1}\n              onClick={() => setPlan(p.sub)}\n              onKeyDown={(e) => arrows(e, 2, subscribe ? 1 : 0, (n) => setPlan(n === 1, true))}\n              className={cn(\n                \"flex min-h-[72px] items-start gap-3 rounded-sk-card border p-4 text-left transition-[border-color,background-color,transform] duration-200 active:scale-[0.97]\",\n                on ? \"border-sk-accent bg-sk-accent/[0.05]\" : \"border-sk-line bg-sk-card hover:border-sk-ink/30\",\n                RING\n              )}\n            >\n              <span className={cn(\"mt-0.5 grid h-[18px] w-[18px] shrink-0 place-items-center rounded-full border\", on ? \"border-sk-accent\" : \"border-sk-line\")}>\n                <motion.span initial={false} animate={{ scale: on ? 1 : 0 }} transition={{ duration: reduce ? 0 : 0.2, ease: EASE }} className=\"h-2.5 w-2.5 rounded-full bg-sk-accent\" />\n              </span>\n              <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n                <span className=\"text-sm font-medium text-sk-ink\">{p.title}</span>\n                <span className=\"text-[13px] text-sk-muted\">{p.note}</span>\n              </span>\n              <span className=\"flex shrink-0 flex-col items-end tabular-nums\">\n                <span className=\"text-sm font-semibold text-sk-price\">{money(p.amount)}</span>\n                {p.sub ? <s className=\"text-[12px] text-sk-muted\">{money(price)}</s> : null}\n              </span>\n            </button>\n          )\n        })}\n      </div>\n\n      <AnimatePresence initial={false}>\n        {subscribe ? (\n          <motion.div key=\"sub\" initial={{ height: 0, opacity: 0 }} animate={{ height: \"auto\", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={t} className=\"overflow-hidden\">\n            <div className=\"flex flex-col gap-4 rounded-sk-card bg-sk-surface p-4\">\n              <div className=\"flex flex-col gap-2\">\n                <span className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">Deliver every</span>\n                <div role=\"radiogroup\" aria-label=\"Delivery frequency\" className=\"grid grid-cols-3 gap-2 sm:flex sm:flex-wrap\">\n                  {frequencies.map((f, i) => {\n                    const on = f === every\n                    return (\n                      <button\n                        key={f}\n                        ref={(el) => {\n                          freqRefs.current[i] = el\n                        }}\n                        type=\"button\"\n                        role=\"radio\"\n                        aria-checked={on}\n                        tabIndex={on ? 0 : -1}\n                        onClick={() => setFreq(i)}\n                        onKeyDown={(e) => arrows(e, frequencies.length, frequencies.indexOf(every), (n) => setFreq(n, true))}\n                        className={cn(\n                          \"h-11 whitespace-nowrap rounded-sk-chip border px-3 text-sm font-medium tabular-nums sm:px-4 transition-[border-color,background-color,color,transform] duration-200 active:scale-[0.97]\",\n                          on ? \"border-sk-accent bg-sk-accent text-sk-accent-ink\" : \"border-sk-line bg-sk-card text-sk-ink hover:border-sk-ink/30\",\n                          RING\n                        )}\n                      >\n                        {f} days\n                      </button>\n                    )\n                  })}\n                </div>\n              </div>\n              <ul className=\"grid gap-2.5 sm:grid-cols-2\">\n                {benefits.map(({ icon: Icon, label }, i) => (\n                  <motion.li\n                    key={label}\n                    initial={{ opacity: 0, y: reduce ? 0 : 6 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    transition={{ duration: reduce ? 0 : 0.35, ease: EASE, delay: reduce ? 0 : 0.08 + i * 0.05 }}\n                    className=\"flex items-center gap-2.5 text-[13px] text-sk-ink\"\n                  >\n                    <Icon size={15} className=\"shrink-0 text-sk-accent\" />\n                    {label}\n                  </motion.li>\n                ))}\n              </ul>\n            </div>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n\n      <p className=\"text-[12px] text-sk-muted\">Cancel anytime. No fees, no minimum term.</p>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/storekit/upsell/upsell-subscribe.tsx"
    }
  ],
  "meta": {
    "section": "upsell",
    "variant": "subscribe",
    "niche": [
      "any"
    ],
    "commerce": [
      "subscribe"
    ],
    "previewHeight": 420
  }
}