{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "upsell-prepaid-cards",
  "type": "registry:block",
  "title": "Prepaid duration cards",
  "description": "30, 60 and 90 day cards stating total today, the monthly equivalent and the saving.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "upsell",
    "supplements",
    "beverage"
  ],
  "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": "upsell/upsell-prepaid-cards.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { Check } from \"lucide-react\"\nimport { cn, money } from \"@/components/storekit/lib/cn\"\nimport { Button } from \"@/components/storekit/ui/button\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Prepaid duration cards. Each plan states three numbers plainly: the total\n   charged today, what that works out to per month, and the saving against\n   paying month to month. Prices are integer cents. */\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\ntype Plan = { id: string; days: number; discount: number; label?: string }\n\nconst DEFAULT_PLANS: Plan[] = [\n  { id: \"30\", days: 30, discount: 0 },\n  { id: \"60\", days: 60, discount: 0.08 },\n  { id: \"90\", days: 90, discount: 0.15, label: \"Lowest per month\" },\n]\n\nexport function UpsellPrepaidCards({\n  monthlyPrice = 5900,\n  plans = DEFAULT_PLANS,\n  defaultPlan = \"90\",\n  onAddToCart,\n  className,\n}: {\n  /** Price of one 30-day supply in integer cents. */\n  monthlyPrice?: number\n  plans?: Plan[]\n  defaultPlan?: string\n  onAddToCart?: (v: { planId: string; days: number; total: number }) => void\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"upsell\", \"upsell-prepaid-cards\")\n  const [sel, setSel] = useState(Math.max(0, plans.findIndex((p) => p.id === defaultPlan)))\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  const calc = (p: Plan) => {\n    const months = p.days / 30\n    const full = Math.round(monthlyPrice * months)\n    const total = Math.round(full * (1 - p.discount))\n    return { months, total, perMonth: Math.round(total / months), saved: full - total }\n  }\n  const plan = plans[sel] ?? plans[0]\n  const cur = calc(plan)\n\n  const choose = (i: number, focus = false) => {\n    if (i !== sel) sk.emit(\"interact\", `prepaid_${plans[i].days}d`, calc(plans[i]).total)\n    setSel(i)\n    if (focus) refs.current[i]?.focus()\n  }\n\n  const add = () => {\n    if (status !== \"idle\") return\n    setStatus(\"loading\")\n    sk.emit(\"convert\", \"add_to_cart\", cur.total)\n    timers.current.push(\n      window.setTimeout(() => {\n        setStatus(\"added\")\n        onAddToCart?.({ planId: plan.id, days: plan.days, total: cur.total })\n        timers.current.push(window.setTimeout(() => setStatus(\"idle\"), 1200))\n      }, 700)\n    )\n  }\n\n  return (\n    <div {...sk.attrs} ref={sk.ref as React.RefObject<HTMLDivElement>} className={cn(\"flex w-full flex-col gap-4\", className)}>\n      <div role=\"radiogroup\" aria-label=\"Prepaid plan\" className=\"grid gap-2.5 sm:grid-cols-3\">\n        {plans.map((p, i) => {\n          const on = i === sel\n          const c = calc(p)\n          return (\n            <button\n              key={p.id}\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) => {\n                const f = e.key === \"ArrowRight\" || e.key === \"ArrowDown\"\n                const b = e.key === \"ArrowLeft\" || e.key === \"ArrowUp\"\n                if (!f && !b) return\n                e.preventDefault()\n                choose((sel + (f ? 1 : -1) + plans.length) % plans.length, true)\n              }}\n              className={cn(\n                \"relative flex flex-col 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=\"flex items-center justify-between gap-2\">\n                <span className=\"font-sk-display text-base font-semibold tracking-[-0.02em] text-sk-ink tabular-nums\">{p.days}-day supply</span>\n                <span className={cn(\"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>\n              <span className=\"flex flex-col gap-0.5 tabular-nums\">\n                <span className=\"text-sm text-sk-muted\">\n                  <span className=\"font-semibold text-sk-price\">{money(c.total)}</span> today\n                </span>\n                <span className=\"text-[13px] text-sk-muted\">{money(c.perMonth)} / month</span>\n              </span>\n              <span className=\"flex flex-wrap items-center gap-1.5\">\n                {c.saved > 0 ? (\n                  <span className=\"rounded-sk-chip bg-sk-savings/[0.12] px-2 py-0.5 text-[12px] font-semibold text-sk-savings tabular-nums\">Save {money(c.saved)}</span>\n                ) : (\n                  <span className=\"rounded-sk-chip bg-sk-surface px-2 py-0.5 text-[12px] font-medium text-sk-muted\">Standard price</span>\n                )}\n                {p.label ? <span className=\"text-[12px] text-sk-muted\">{p.label}</span> : null}\n              </span>\n            </button>\n          )\n        })}\n      </div>\n\n      <div className=\"flex flex-col gap-3 rounded-sk-card bg-sk-surface p-4\">\n        <dl className=\"flex flex-col gap-1.5 text-sm tabular-nums\">\n          <div className=\"flex justify-between gap-4\">\n            <dt className=\"text-sk-muted\">Total today</dt>\n            <dd className=\"font-semibold text-sk-ink\">{money(cur.total)}</dd>\n          </div>\n          <div className=\"flex justify-between gap-4\">\n            <dt className=\"text-sk-muted\">Works out to</dt>\n            <dd className=\"text-sk-ink\">{money(cur.perMonth)} / month</dd>\n          </div>\n          <div className=\"flex justify-between gap-4\">\n            <dt className=\"text-sk-muted\">Versus monthly</dt>\n            <dd className={cur.saved > 0 ? \"font-medium text-sk-savings\" : \"text-sk-muted\"}>{cur.saved > 0 ? `${money(cur.saved)} saved` : \"Same price\"}</dd>\n          </div>\n        </dl>\n        <Button size=\"lg\" className=\"w-full active:scale-[0.97]\" loading={status === \"loading\"} onClick={add}>\n          <AnimatePresence mode=\"wait\" initial={false}>\n            <motion.span\n              key={status === \"added\" ? \"added\" : plan.id}\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} /> Added to cart\n                </>\n              ) : (\n                <>Prepay {plan.days} days · {money(cur.total)}</>\n              )}\n            </motion.span>\n          </AnimatePresence>\n        </Button>\n        <p className=\"text-center text-[12px] text-sk-muted\">One charge. Does not renew automatically.</p>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "components/storekit/upsell/upsell-prepaid-cards.tsx"
    }
  ],
  "meta": {
    "section": "upsell",
    "variant": "prepaid",
    "niche": [
      "supplements",
      "beverage"
    ],
    "commerce": [
      "prepaid",
      "atc"
    ],
    "previewHeight": 420
  }
}