{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cart-gift-unlock",
  "type": "registry:component",
  "title": "Gift unlock",
  "description": "Locked row that names the amount left, then opens a choose-one-of-three gift picker at the threshold.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "cart",
    "any"
  ],
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://storekit-eight.vercel.app/r/cn.json",
    "https://storekit-eight.vercel.app/r/product-image.json",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "cart/cart-gift-unlock.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { Check, Gift, Lock } from \"lucide-react\"\nimport { cn, money } from \"@/components/storekit/lib/cn\"\nimport { ProductImage } from \"@/components/storekit/ui/product-image\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Gift unlock. Below the threshold it is a quiet locked row with the amount\n   left. At the threshold a chooser opens with three gifts; the shopper picks\n   one and it joins the order at no charge. Dropping below the threshold\n   releases the pick. */\n\nexport type CartGift = { id: string; title: string; detail?: string; value: number; image: string }\n\nconst SAMPLE_GIFTS: CartGift[] = [\n  { id: \"travel-sticks\", title: \"Electrolyte sticks\", detail: \"5 sticks\", value: 1200, image: \"/brands/morrow/electrolytes.webp\" },\n  { id: \"greens-travel\", title: \"Daily Greens\", detail: \"Travel size\", value: 1800, image: \"/brands/morrow/greens.webp\" },\n  { id: \"magnesium-trial\", title: \"Magnesium Night\", detail: \"7-night pack\", value: 1500, image: \"/brands/morrow/magnesium.webp\" },\n]\n\nexport function CartGiftUnlock({\n  subtotal = 16200,\n  threshold = 15000,\n  gifts = SAMPLE_GIFTS,\n  defaultGiftId,\n  onChoose,\n  className,\n}: {\n  subtotal?: number\n  threshold?: number\n  gifts?: CartGift[]\n  defaultGiftId?: string\n  onChoose?: (gift: CartGift | null) => void\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"cart\", \"cart-gift-unlock\")\n  const [picked, setPicked] = React.useState<string | undefined>(defaultGiftId)\n  const unlocked = subtotal >= threshold\n  const remaining = Math.max(0, threshold - subtotal)\n  const groupId = React.useId()\n\n  React.useEffect(() => {\n    if (!unlocked && picked) {\n      setPicked(undefined)\n      onChoose?.(null)\n    }\n  }, [unlocked, picked, onChoose])\n\n  const choose = (g: CartGift) => {\n    setPicked(g.id)\n    sk.emit(\"convert\", \"gift_select\", g.value)\n    onChoose?.(g)\n  }\n\n  return (\n    <div ref={sk.ref as React.Ref<HTMLDivElement>} {...sk.attrs} className={cn(\"w-full overflow-hidden rounded-sk-card border border-sk-line bg-sk-card\", className)}>\n      <div className=\"flex items-center gap-3 p-4\">\n        <span className={cn(\"grid h-9 w-9 shrink-0 place-items-center rounded-sk-chip transition-colors duration-300\", unlocked ? \"bg-sk-accent text-sk-accent-ink\" : \"bg-sk-ink/[0.06] text-sk-muted\")}>\n          {unlocked ? <Gift size={16} /> : <Lock size={15} />}\n        </span>\n        <div className=\"min-w-0 flex-1\" aria-live=\"polite\">\n          {unlocked ? (\n            <>\n              <p id={groupId} className=\"text-sm font-medium text-sk-ink\">Choose your free gift</p>\n              <p className=\"text-[13px] text-sk-muted\">{picked ? \"Added to your order at no charge.\" : \"Pick one. It ships with this order.\"}</p>\n            </>\n          ) : (\n            <>\n              <p className=\"text-sm font-medium text-sk-ink\">\n                Spend <span className=\"tabular-nums\">{money(remaining)}</span> more for a free gift\n              </p>\n              <p className=\"text-[13px] text-sk-muted\">You will choose from {gifts.length} options.</p>\n            </>\n          )}\n        </div>\n      </div>\n\n      <AnimatePresence initial={false}>\n        {unlocked ? (\n          <motion.div\n            key=\"gifts\"\n            initial={{ height: 0, opacity: 0 }}\n            animate={{ height: \"auto\", opacity: 1 }}\n            exit={{ height: 0, opacity: 0 }}\n            transition={{ duration: reduce ? 0 : 0.4, ease: [0.16, 1, 0.3, 1] }}\n            className=\"overflow-hidden\"\n          >\n            <div role=\"radiogroup\" aria-labelledby={groupId} className=\"grid grid-cols-3 gap-2 border-t border-sk-line p-3 sm:gap-3 sm:p-4\">\n              {gifts.slice(0, 3).map((g, i) => {\n                const on = picked === g.id\n                return (\n                  <motion.button\n                    key={g.id}\n                    type=\"button\"\n                    role=\"radio\"\n                    aria-checked={on}\n                    onClick={() => choose(g)}\n                    initial={reduce ? false : { opacity: 0, y: 8 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    transition={{ duration: reduce ? 0 : 0.4, delay: reduce ? 0 : 0.08 + i * 0.05, ease: [0.16, 1, 0.3, 1] }}\n                    className={cn(\n                      \"relative flex min-w-0 flex-col rounded-sk-card border p-2 text-left transition-[border-color,background-color,transform] duration-200 active:scale-[0.97] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-card\",\n                      on ? \"border-sk-accent bg-sk-accent/[0.06]\" : \"border-sk-line hover:border-sk-ink/30\"\n                    )}\n                  >\n                    <ProductImage src={g.image} alt=\"\" className=\"w-full rounded-sk-card\" zoomOnHover={false} />\n                    <span className=\"mt-2 line-clamp-2 hyphens-auto break-words text-[13px] font-medium leading-tight text-sk-ink\">{g.title}</span>\n                    {g.detail ? <span className=\"text-xs text-sk-muted\">{g.detail}</span> : null}\n                    <span className=\"mt-1 text-xs tabular-nums text-sk-muted\">\n                      <s>{money(g.value)}</s> <span className=\"font-medium text-sk-savings\">Free</span>\n                    </span>\n                    <span\n                      aria-hidden\n                      className={cn(\n                        \"absolute right-1.5 top-1.5 grid h-5 w-5 place-items-center rounded-sk-chip border transition-colors duration-200\",\n                        on ? \"border-sk-accent bg-sk-accent text-sk-accent-ink\" : \"border-sk-line bg-sk-card\"\n                      )}\n                    >\n                      {on ? <Check size={12} strokeWidth={3} /> : null}\n                    </span>\n                  </motion.button>\n                )\n              })}\n            </div>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/storekit/cart/cart-gift-unlock.tsx"
    }
  ],
  "meta": {
    "section": "cart",
    "variant": "gift",
    "niche": [
      "any"
    ],
    "commerce": [
      "gift",
      "shipping-threshold"
    ],
    "previewHeight": 420
  }
}