{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pdp-buybox",
  "type": "registry:block",
  "title": "Buy box",
  "description": "Rating, title, live price, variant pills with sold out state, subscribe and save, quantity, add to cart, trust row and accordion.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "pdp",
    "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/stars.json",
    "https://storekit-eight.vercel.app/r/stepper.json",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "pdp/pdp-buybox.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useId, useRef, useState } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { Check, ChevronDown, FileCheck2, ShieldCheck, Truck } 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 { Stars } from \"@/components/storekit/ui/stars\"\nimport { Stepper } from \"@/components/storekit/ui/stepper\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* The product page buy box: rating, title, live price, variant pills,\n   one-time vs subscribe, quantity, add to cart with confirmation, a trust row\n   and an accordion. Prices are integer cents. */\n\ntype Variant = { id: string; label: string; price: number; compareAt?: number; soldOut?: boolean }\ntype Detail = { title: string; body: React.ReactNode }\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_VARIANTS: Variant[] = [\n  { id: \"30\", label: \"30 servings\", price: 4900, compareAt: 5900 },\n  { id: \"60\", label: \"60 servings\", price: 8900, compareAt: 11800 },\n  { id: \"bundle\", label: \"3-pouch bundle\", price: 12900, soldOut: true },\n]\n\nconst DEFAULT_DETAILS: Detail[] = [\n  { title: \"Details\", body: \"28 greens, adaptogens and probiotics in one scoop. 30 servings per pouch, vegan and gluten free, made in the USA.\" },\n  { title: \"Shipping\", body: \"Orders placed before 2pm ET ship the same day. Free shipping on subscriptions and orders over $75.\" },\n  { title: \"Lab results\", body: \"Every batch is third-party tested for heavy metals, pesticides and microbials. Results are posted for each batch code.\" },\n]\n\nconst FREQUENCIES = [30, 60, 90]\n\nfunction onRadioKeys<T>(e: React.KeyboardEvent, items: T[], current: number, enabled: (t: T) => boolean, pick: (i: number) => void) {\n  const forward = e.key === \"ArrowRight\" || e.key === \"ArrowDown\"\n  const back = e.key === \"ArrowLeft\" || e.key === \"ArrowUp\"\n  if (!forward && !back) return\n  e.preventDefault()\n  const n = items.length\n  for (let step = 1; step <= n; step++) {\n    const i = (current + (forward ? step : -step) + n * 2) % n\n    if (enabled(items[i])) return pick(i)\n  }\n}\n\nexport function PdpBuybox({\n  category = \"Powders\",\n  title = \"Daily Greens\",\n  rating = 4.8,\n  reviewCount = 1284,\n  variants = DEFAULT_VARIANTS,\n  subscribeDiscount = 0.15,\n  details = DEFAULT_DETAILS,\n  onAddToCart,\n  className,\n}: {\n  category?: string\n  title?: string\n  rating?: number\n  reviewCount?: number\n  variants?: Variant[]\n  /** 0.15 = 15% off when subscribed. */\n  subscribeDiscount?: number\n  details?: Detail[]\n  onAddToCart?: (line: { variantId: string; quantity: number; subscribe: boolean; everyDays?: number }) => void\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const uid = useId()\n  const sk = useSkBlock(\"pdp\", \"pdp-buybox\")\n  const firstAvailable = Math.max(0, variants.findIndex((v) => !v.soldOut))\n  const [vIndex, setVIndex] = useState(firstAvailable)\n  const [subscribe, setSubscribe] = useState(true)\n  const [every, setEvery] = useState(30)\n  const [qty, setQty] = useState(1)\n  const [status, setStatus] = useState<\"idle\" | \"loading\" | \"added\">(\"idle\")\n  const [open, setOpen] = useState<number | null>(0)\n  const pillRefs = useRef<(HTMLButtonElement | null)[]>([])\n  const planRefs = useRef<(HTMLButtonElement | null)[]>([])\n  const timers = useRef<number[]>([])\n\n  useEffect(() => () => timers.current.forEach((t) => window.clearTimeout(t)), [])\n\n  const variant = variants[vIndex] ?? variants[0]\n  const pct = Math.round(subscribeDiscount * 100)\n  const subUnit = Math.round(variant.price * (1 - subscribeDiscount))\n  const unit = subscribe ? subUnit : variant.price\n  const shownCompare = subscribe ? variant.compareAt ?? variant.price : variant.compareAt\n  const t = { duration: reduce ? 0 : 0.35, ease: EASE }\n\n  const chooseVariant = (i: number) => {\n    if (i !== vIndex) sk.emit(\"interact\", \"variant\", undefined)\n    setVIndex(i)\n  }\n  const choosePlan = (sub: boolean) => {\n    if (sub !== subscribe) sk.emit(\"interact\", sub ? \"subscribe\" : \"one_time\", undefined)\n    setSubscribe(sub)\n  }\n  const pickVariant = (i: number) => {\n    chooseVariant(i)\n    pillRefs.current[i]?.focus()\n  }\n  const pickPlan = (i: number) => {\n    choosePlan(i === 1)\n    planRefs.current[i]?.focus()\n  }\n\n  const add = () => {\n    if (status !== \"idle\" || variant.soldOut) return\n    setStatus(\"loading\")\n    sk.emit(\"convert\", \"add_to_cart\", unit * qty)\n    timers.current.push(\n      window.setTimeout(() => {\n        setStatus(\"added\")\n        onAddToCart?.({ variantId: variant.id, quantity: qty, subscribe, everyDays: subscribe ? every : 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 gap-6\", className)}>\n      <div className=\"flex flex-col gap-3\">\n        <div className=\"flex flex-wrap items-center justify-between gap-2\">\n          <span className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">{category}</span>\n          <Stars value={rating} count={reviewCount} size={13} showValue />\n        </div>\n        <h1 className=\"font-sk-display text-3xl font-semibold leading-[1.1] tracking-[-0.02em] text-sk-ink sm:text-4xl\">{title}</h1>\n        <div className=\"relative h-9 overflow-hidden\" aria-live=\"polite\">\n          <AnimatePresence mode=\"popLayout\" initial={false}>\n            <motion.div\n              key={`${variant.id}-${unit}`}\n              initial={{ opacity: 0, y: reduce ? 0 : 10 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: reduce ? 0 : -10 }}\n              transition={t}\n              className=\"absolute inset-x-0 top-0\"\n            >\n              <Price amount={unit} compareAt={shownCompare} size=\"lg\" showSavings />\n            </motion.div>\n          </AnimatePresence>\n        </div>\n      </div>\n\n      <fieldset className=\"flex flex-col gap-2.5\">\n        <legend className=\"mb-2.5 flex w-full items-center justify-between text-sm\">\n          <span className=\"font-medium text-sk-ink\">Size</span>\n          <span className=\"text-sk-muted\">{variant.label}</span>\n        </legend>\n        <div role=\"radiogroup\" aria-label=\"Size\" className=\"flex flex-wrap gap-2\">\n          {variants.map((v, i) => {\n            const selected = i === vIndex\n            return (\n              <span key={v.id} className=\"group/pill relative\">\n                <button\n                  ref={(el) => {\n                    pillRefs.current[i] = el\n                  }}\n                  type=\"button\"\n                  role=\"radio\"\n                  aria-checked={selected}\n                  aria-disabled={v.soldOut || undefined}\n                  aria-describedby={v.soldOut ? `${uid}-so-${i}` : undefined}\n                  tabIndex={selected ? 0 : -1}\n                  onClick={() => !v.soldOut && chooseVariant(i)}\n                  onKeyDown={(e) => onRadioKeys(e, variants, vIndex, (x) => !x.soldOut, pickVariant)}\n                  className={cn(\n                    \"h-11 min-w-[72px] rounded-sk-btn border px-4 text-sm font-medium tabular-nums transition-[border-color,background-color,color] duration-200 ease-sk-out\",\n                    selected ? \"border-sk-accent bg-sk-accent/[0.08] text-sk-ink\" : \"border-sk-line text-sk-ink hover:border-sk-ink/40\",\n                    v.soldOut && \"cursor-not-allowed text-sk-muted line-through decoration-sk-muted/70 hover:border-sk-line\",\n                    RING\n                  )}\n                >\n                  {v.label}\n                </button>\n                {v.soldOut ? (\n                  <span\n                    id={`${uid}-so-${i}`}\n                    role=\"tooltip\"\n                    className=\"pointer-events-none absolute -top-8 left-1/2 -translate-x-1/2 origin-bottom scale-95 whitespace-nowrap rounded-sk-chip bg-sk-ink px-2 py-1 text-[11px] font-medium text-sk-paper opacity-0 transition-[opacity,transform] duration-150 ease-sk-out group-hover/pill:scale-100 group-hover/pill:opacity-100 motion-reduce:transition-none\"\n                  >\n                    Sold out\n                  </span>\n                ) : null}\n              </span>\n            )\n          })}\n        </div>\n      </fieldset>\n\n      <div role=\"radiogroup\" aria-label=\"Purchase option\" className=\"flex flex-col gap-2.5\">\n        {[false, true].map((isSub, i) => {\n          const selected = subscribe === isSub\n          return (\n            <div\n              key={String(isSub)}\n              className={cn(\n                \"overflow-hidden rounded-sk-card border transition-[border-color,background-color] duration-200 ease-sk-out\",\n                selected ? \"border-sk-accent bg-sk-accent/[0.04]\" : \"border-sk-line bg-sk-card hover:border-sk-ink/30\"\n              )}\n            >\n              <button\n                ref={(el) => {\n                  planRefs.current[i] = el\n                }}\n                type=\"button\"\n                role=\"radio\"\n                aria-checked={selected}\n                tabIndex={selected ? 0 : -1}\n                onClick={() => choosePlan(isSub)}\n                onKeyDown={(e) => onRadioKeys(e, [0, 1], subscribe ? 1 : 0, () => true, pickPlan)}\n                className={cn(\"flex min-h-[56px] w-full items-center gap-3 rounded-sk-card px-4 py-3 text-left\", RING)}\n              >\n                <span className={cn(\"grid h-[18px] w-[18px] shrink-0 place-items-center rounded-full border transition-colors duration-200\", selected ? \"border-sk-accent\" : \"border-sk-line\")}>\n                  <motion.span\n                    initial={false}\n                    animate={{ scale: selected ? 1 : 0 }}\n                    transition={{ duration: reduce ? 0 : 0.25, ease: EASE }}\n                    className=\"h-2.5 w-2.5 rounded-full bg-sk-accent\"\n                  />\n                </span>\n                <span className=\"flex min-w-0 flex-1 flex-col\">\n                  <span className=\"text-sm font-medium text-sk-ink\">{isSub ? `Subscribe and save ${pct}%` : \"One-time purchase\"}</span>\n                  {isSub ? <span className=\"text-[13px] text-sk-muted\">Skip or cancel anytime</span> : null}\n                </span>\n                <span className=\"shrink-0 text-sm font-semibold tabular-nums text-sk-price\">{money(isSub ? subUnit : variant.price)}</span>\n              </button>\n              {isSub ? (\n                <AnimatePresence initial={false}>\n                  {selected ? (\n                    <motion.div\n                      key=\"freq\"\n                      initial={{ height: 0, opacity: 0 }}\n                      animate={{ height: \"auto\", opacity: 1 }}\n                      exit={{ height: 0, opacity: 0 }}\n                      transition={t}\n                    >\n                      <div className=\"flex flex-wrap items-center justify-between gap-3 border-t border-sk-line/70 px-4 py-3\">\n                        <label className=\"flex items-center gap-2 text-[13px] text-sk-muted\">\n                          Deliver every\n                          <span className=\"relative\">\n                            <select\n                              value={every}\n                              onChange={(e) => setEvery(Number(e.target.value))}\n                              className={cn(\"h-11 appearance-none rounded-sk-btn border border-sk-line bg-sk-card pl-3 pr-8 text-base text-sk-ink sm:text-sm\", RING)}\n                            >\n                              {FREQUENCIES.map((f) => (\n                                <option key={f} value={f}>\n                                  {f} days\n                                </option>\n                              ))}\n                            </select>\n                            <ChevronDown size={14} className=\"pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-sk-muted\" />\n                          </span>\n                        </label>\n                        <span className=\"text-[13px] text-sk-muted\">\n                          <span className=\"font-semibold tabular-nums text-sk-ink\">{money(subUnit * qty)}</span> per delivery\n                        </span>\n                      </div>\n                    </motion.div>\n                  ) : null}\n                </AnimatePresence>\n              ) : null}\n            </div>\n          )\n        })}\n      </div>\n\n      <div className=\"flex flex-col gap-3\">\n        <div className=\"flex gap-3\">\n          <Stepper value={qty} onChange={setQty} max={20} className=\"shrink-0\" />\n          <Button size=\"lg\" className=\"h-auto min-h-11 min-w-0 flex-1 whitespace-normal px-4 py-2 text-center leading-tight sm:px-6\" loading={status === \"loading\"} disabled={variant.soldOut} onClick={add}>\n            <AnimatePresence mode=\"wait\" initial={false}>\n              <motion.span\n                key={status === \"added\" ? \"added\" : \"add\"}\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 flex-wrap items-center justify-center gap-x-1.5 gap-y-0.5\"\n              >\n                {status === \"added\" ? (\n                  <>\n                    <Check size={16} strokeWidth={2.5} /> Added to cart\n                  </>\n                ) : (\n                  <>\n                    <span className=\"whitespace-nowrap\">Add to cart</span>\n                    <span className=\"whitespace-nowrap\">\n                      <span aria-hidden className=\"mr-1.5 hidden sm:inline\">·</span>\n                      {money(unit * qty)}\n                    </span>\n                  </>\n                )}\n              </motion.span>\n            </AnimatePresence>\n          </Button>\n        </div>\n        <ul className=\"grid grid-cols-3 gap-2 text-[12px] text-sk-muted\">\n          {[\n            { icon: FileCheck2, label: \"Third-party tested\" },\n            { icon: Truck, label: \"Ships same day\" },\n            { icon: ShieldCheck, label: \"30-day guarantee\" },\n          ].map(({ icon: Icon, label }) => (\n            <li key={label} className=\"flex flex-col items-center justify-center gap-1.5 rounded-sk-card bg-sk-surface px-2 py-2.5 text-center leading-tight\">\n              <Icon size={15} className=\"shrink-0 text-sk-ink\" aria-hidden />\n              {label}\n            </li>\n          ))}\n        </ul>\n      </div>\n\n      <div className=\"border-t border-sk-line\">\n        {details.map((d, i) => {\n          const isOpen = open === i\n          const panelId = `${uid}-acc-${i}`\n          return (\n            <div key={d.title} className=\"border-b border-sk-line\">\n              <h3>\n                <button\n                  type=\"button\"\n                  aria-expanded={isOpen}\n                  aria-controls={panelId}\n                  onClick={() => setOpen(isOpen ? null : i)}\n                  className={cn(\"flex min-h-[48px] w-full items-center justify-between gap-4 py-3 text-left text-sm font-medium text-sk-ink\", RING)}\n                >\n                  {d.title}\n                  <motion.span animate={{ rotate: isOpen ? 180 : 0 }} transition={{ duration: reduce ? 0 : 0.25, ease: EASE }} className=\"text-sk-muted\">\n                    <ChevronDown size={16} />\n                  </motion.span>\n                </button>\n              </h3>\n              <AnimatePresence initial={false}>\n                {isOpen ? (\n                  <motion.div\n                    id={panelId}\n                    key=\"panel\"\n                    initial={{ height: 0, opacity: 0 }}\n                    animate={{ height: \"auto\", opacity: 1 }}\n                    exit={{ height: 0, opacity: 0 }}\n                    transition={t}\n                    className=\"overflow-hidden\"\n                  >\n                    <div className=\"pb-4 text-sm leading-[1.5] text-sk-muted\">{d.body}</div>\n                  </motion.div>\n                ) : null}\n              </AnimatePresence>\n            </div>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "components/storekit/pdp/pdp-buybox.tsx"
    }
  ],
  "docs": "Pass variants in integer cents. Mark a variant soldOut to strike it and skip it in keyboard navigation.",
  "meta": {
    "section": "pdp",
    "variant": "buybox",
    "niche": [
      "any"
    ],
    "commerce": [
      "variants",
      "atc",
      "subscribe",
      "quantity"
    ],
    "previewHeight": 860
  }
}