{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "upsell-purchase-tabs",
  "type": "registry:block",
  "title": "One-time or subscribe tabs",
  "description": "Two tabs that each carry their own price, benefits and call to action, with a per-serving line.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "upsell",
    "supplements",
    "beverage",
    "beauty"
  ],
  "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-purchase-tabs.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useId, 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 { Price } from \"@/components/storekit/ui/price\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* One-time vs subscribe as two tabs. Each tab owns its price, its benefit\n   list and its call to action, so the shopper compares two complete offers\n   instead of a radio and a footnote. 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 Mode = \"once\" | \"subscribe\"\n\nconst DEFAULT_BENEFITS: Record<Mode, string[]> = {\n  once: [\"Ships once, no commitment\", \"Third-party tested, lab results online\", \"Free shipping over $50\"],\n  subscribe: [\"15% off every delivery\", \"Free shipping on every order\", \"Skip, pause or cancel from your account\", \"Fresh batch each shipment\"],\n}\n\nexport function UpsellPurchaseTabs({\n  price = 5900,\n  discount = 0.15,\n  everyDays = 30,\n  servings,\n  benefits = DEFAULT_BENEFITS,\n  defaultMode = \"subscribe\",\n  onAddToCart,\n  className,\n}: {\n  price?: number\n  /** 0.15 = 15% off when subscribed. */\n  discount?: number\n  everyDays?: number\n  /** Servings per unit. When set, a per-serving price shows under each price. */\n  servings?: number\n  benefits?: Record<Mode, string[]>\n  defaultMode?: Mode\n  onAddToCart?: (line: { subscribe: boolean; price: number; everyDays?: number }) => void\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const uid = useId()\n  const sk = useSkBlock(\"upsell\", \"upsell-purchase-tabs\")\n  const [mode, setMode] = useState<Mode>(defaultMode)\n  const [status, setStatus] = useState<\"idle\" | \"loading\" | \"added\">(\"idle\")\n  const tabRefs = useRef<(HTMLButtonElement | null)[]>([])\n  const timers = useRef<number[]>([])\n  useEffect(() => () => timers.current.forEach((t) => window.clearTimeout(t)), [])\n\n  const pct = Math.round(discount * 100)\n  const subPrice = Math.round(price * (1 - discount))\n  const amount = mode === \"subscribe\" ? subPrice : price\n  const modes: { id: Mode; label: string }[] = [\n    { id: \"once\", label: \"One-time\" },\n    { id: \"subscribe\", label: `Subscribe, save ${pct}%` },\n  ]\n\n  const pick = (m: Mode, focus = false) => {\n    if (m !== mode) sk.emit(\"interact\", m === \"subscribe\" ? \"subscribe\" : \"one_time\", m === \"subscribe\" ? subPrice : price)\n    setMode(m)\n    if (focus) tabRefs.current[m === \"once\" ? 0 : 1]?.focus()\n  }\n\n  const add = () => {\n    if (status !== \"idle\") return\n    setStatus(\"loading\")\n    sk.emit(\"convert\", \"add_to_cart\", amount)\n    timers.current.push(\n      window.setTimeout(() => {\n        setStatus(\"added\")\n        onAddToCart?.({ subscribe: mode === \"subscribe\", price: amount, everyDays: mode === \"subscribe\" ? everyDays : undefined })\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 overflow-hidden rounded-sk-card border border-sk-line bg-sk-card\", className)}>\n      <div role=\"tablist\" aria-label=\"Purchase option\" className=\"grid grid-cols-2 gap-1 border-b border-sk-line bg-sk-surface p-1\">\n        {modes.map((m, i) => {\n          const on = m.id === mode\n          return (\n            <button\n              key={m.id}\n              ref={(el) => {\n                tabRefs.current[i] = el\n              }}\n              id={`${uid}-tab-${m.id}`}\n              type=\"button\"\n              role=\"tab\"\n              aria-selected={on}\n              aria-controls={`${uid}-panel`}\n              tabIndex={on ? 0 : -1}\n              onClick={() => pick(m.id)}\n              onKeyDown={(e) => {\n                if ([\"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"].includes(e.key)) {\n                  e.preventDefault()\n                  pick(mode === \"once\" ? \"subscribe\" : \"once\", true)\n                }\n              }}\n              className={cn(\n                \"relative h-11 rounded-sk-btn px-3 text-sm font-medium transition-[color,transform] duration-200 active:scale-[0.97]\",\n                on ? \"text-sk-ink\" : \"text-sk-muted hover:text-sk-ink\",\n                RING\n              )}\n            >\n              {on ? (\n                <motion.span\n                  layoutId={`${uid}-pill`}\n                  transition={{ duration: reduce ? 0 : 0.35, ease: EASE }}\n                  className=\"absolute inset-0 rounded-sk-btn border border-sk-line bg-sk-card\"\n                />\n              ) : null}\n              <span className=\"relative\">{m.label}</span>\n            </button>\n          )\n        })}\n      </div>\n\n      <div id={`${uid}-panel`} role=\"tabpanel\" aria-labelledby={`${uid}-tab-${mode}`} className=\"flex flex-col gap-5 p-5 sm:p-6\">\n        <AnimatePresence mode=\"wait\" initial={false}>\n          <motion.div\n            key={mode}\n            initial={{ opacity: 0, y: reduce ? 0 : 8 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: reduce ? 0 : -6 }}\n            transition={{ duration: reduce ? 0 : 0.3, ease: EASE }}\n            className=\"flex flex-col gap-4\"\n          >\n            <div className=\"flex flex-col gap-1\">\n              <Price\n                amount={amount}\n                compareAt={mode === \"subscribe\" ? price : undefined}\n                size=\"lg\"\n                showSavings\n                per={servings ? { amount: Math.round(amount / servings), unit: \"serving\" } : undefined}\n              />\n              <span className=\"text-[13px] text-sk-muted\">\n                {mode === \"subscribe\" ? `Billed per delivery, every ${everyDays} days` : \"Charged once at checkout\"}\n              </span>\n            </div>\n            <ul className=\"flex flex-col gap-2.5\">\n              {benefits[mode].map((b) => (\n                <li key={b} className=\"flex items-start gap-2.5 text-sm text-sk-ink\">\n                  <Check size={16} strokeWidth={2.25} className=\"mt-0.5 shrink-0 text-sk-accent\" aria-hidden />\n                  {b}\n                </li>\n              ))}\n            </ul>\n          </motion.div>\n        </AnimatePresence>\n\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\" : mode}\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              ) : mode === \"subscribe\" ? (\n                <>Start subscription · {money(amount)}</>\n              ) : (\n                <>Add to cart · {money(amount)}</>\n              )}\n            </motion.span>\n          </AnimatePresence>\n        </Button>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "components/storekit/upsell/upsell-purchase-tabs.tsx"
    }
  ],
  "meta": {
    "section": "upsell",
    "variant": "purchase-tabs",
    "niche": [
      "supplements",
      "beverage",
      "beauty"
    ],
    "commerce": [
      "subscribe",
      "atc"
    ],
    "previewHeight": 460
  }
}