{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "product-card-quickadd",
  "type": "registry:component",
  "title": "Quick-add product card",
  "description": "Size chips over the image swap the price with a crossfade; stepper and Add in the footer; stock badge.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "product-card",
    "supplements",
    "wellness"
  ],
  "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/badge.json",
    "https://storekit-eight.vercel.app/r/stepper.json",
    "https://storekit-eight.vercel.app/r/product-image.json",
    "https://storekit-eight.vercel.app/r/product-card-minimal.json",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "product-card/product-card-quickadd.tsx",
      "content": "\"use client\"\n\nimport { useState } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { Check } from \"lucide-react\"\nimport { cn } from \"@/components/storekit/lib/cn\"\nimport { Badge } from \"@/components/storekit/ui/badge\"\nimport { Button } from \"@/components/storekit/ui/button\"\nimport { Price } from \"@/components/storekit/ui/price\"\nimport { ProductImage } from \"@/components/storekit/ui/product-image\"\nimport { Stepper } from \"@/components/storekit/ui/stepper\"\nimport { useAddState } from \"@/components/storekit/product-card/product-card-minimal\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Quick-add tile. Size chips rise over the image on hover (always shown on\n   touch widths); picking a size swaps the price with a short crossfade. The\n   footer holds a quantity stepper and Add. One stock badge, top-left. */\n\nconst EASE = [0.23, 1, 0.32, 1] as const\n\ntype Dose = { label: string; price: number; compareAt?: number }\n\nexport function ProductCardQuickadd({\n  title = \"Daily Greens\",\n  image = \"/placeholder.webp\",\n  href = \"#\",\n  doses = [\n    { label: \"15 servings\", price: 2900 },\n    { label: \"30 servings\", price: 4900 },\n    { label: \"60 servings\", price: 8900 },\n  ],\n  defaultDose = 1,\n  stock = \"in\",\n  onAdd,\n  className,\n}: {\n  title?: string\n  image?: string\n  href?: string\n  doses?: Dose[]\n  defaultDose?: number\n  stock?: \"in\" | \"low\" | \"out\"\n  onAdd?: (line: { title: string; dose: string; quantity: number; price: number }) => void | Promise<void>\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const [doseIndex, setDoseIndex] = useState(Math.min(defaultDose, doses.length - 1))\n  const [qty, setQty] = useState(1)\n  const dose = doses[doseIndex]\n  const sk = useSkBlock(\"product-card\", \"product-card-quickadd\")\n  const { state, add } = useAddState(() => {\n    sk.emit(\"convert\", \"add_to_cart\", dose.price * qty)\n    return onAdd?.({ title, dose: dose.label, quantity: qty, price: dose.price })\n  })\n  const soldOut = stock === \"out\"\n\n  return (\n    <article ref={sk.ref} {...sk.attrs} className={cn(\"group/img relative flex flex-col gap-3\", className)}>\n      <div className=\"relative\">\n        <ProductImage src={image} alt=\"\" fit=\"cover\" className=\"rounded-sk-card\" />\n        <div className=\"absolute left-3 top-3 overflow-hidden rounded-sk-chip bg-sk-card\">\n          {stock === \"in\" ? (\n            <Badge tone=\"success\" dot>In stock</Badge>\n          ) : stock === \"low\" ? (\n            <Badge tone=\"sale\">Low stock</Badge>\n          ) : (\n            <Badge tone=\"outline\">Sold out</Badge>\n          )}\n        </div>\n        <div\n          role=\"radiogroup\"\n          aria-label=\"Dose\"\n          className={cn(\n            \"absolute inset-x-3 bottom-3 z-10 flex gap-1.5\",\n            \"transition-[opacity,transform] duration-200 ease-sk-out\",\n            \"lg:translate-y-1 lg:opacity-0 lg:group-hover/img:translate-y-0 lg:group-hover/img:opacity-100 lg:group-focus-within/img:translate-y-0 lg:group-focus-within/img:opacity-100\",\n            \"motion-reduce:translate-y-0\"\n          )}\n        >\n          {doses.map((d, i) => {\n            const active = i === doseIndex\n            return (\n              <button\n                key={d.label}\n                type=\"button\"\n                role=\"radio\"\n                aria-checked={active}\n                onClick={() => setDoseIndex(i)}\n                className={cn(\n                  \"h-11 flex-1 rounded-sk-chip border font-sk-mono text-[11px] uppercase tracking-[0.08em] outline-none transition-[transform,background-color,color] duration-200 ease-sk-out active:scale-[0.97] lg:h-9\",\n                  \"focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper\",\n                  active ? \"border-sk-ink bg-sk-ink text-sk-paper\" : \"border-sk-line bg-sk-card text-sk-ink hover:border-sk-ink/40\"\n                )}\n              >\n                {d.label}\n              </button>\n            )\n          })}\n        </div>\n      </div>\n\n      <div className=\"flex items-baseline justify-between gap-3\">\n        <h3 className=\"min-w-0 truncate text-[15px] font-medium text-sk-ink\">\n          <a\n            href={href}\n            className=\"outline-none after:absolute after:inset-0 after:rounded-sk-card after:content-[''] focus-visible:after:ring-2 focus-visible:after:ring-sk-accent focus-visible:after:ring-offset-2 focus-visible:after:ring-offset-sk-paper\"\n          >\n            {title}\n          </a>\n          <span className=\"ml-2 font-sk-mono text-[11px] uppercase tracking-[0.08em] text-sk-muted\">{dose.label}</span>\n        </h3>\n        <span className=\"relative inline-grid overflow-hidden\" aria-live=\"polite\">\n          <AnimatePresence mode=\"popLayout\" initial={false}>\n            <motion.span\n              key={dose.label}\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: 0.25, ease: EASE }}\n              className=\"col-start-1 row-start-1 justify-self-end\"\n            >\n              <Price amount={dose.price} compareAt={dose.compareAt} size=\"sm\" />\n            </motion.span>\n          </AnimatePresence>\n        </span>\n      </div>\n\n      <div className=\"relative z-10 flex items-center gap-2\">\n        <Stepper value={qty} onChange={setQty} size=\"sm\" className=\"h-11 lg:h-9\" max={soldOut ? 1 : 20} />\n        <Button\n          size=\"sm\"\n          className=\"h-11 flex-1 active:scale-[0.97] lg:h-9\"\n          disabled={soldOut}\n          loading={state === \"loading\"}\n          onClick={add}\n          aria-label={`Add ${qty} ${title} ${dose.label} to cart`}\n        >\n          {soldOut ? \"Sold out\" : state === \"added\" ? (<><Check size={14} aria-hidden />Added</>) : \"Add\"}\n        </Button>\n      </div>\n    </article>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/storekit/product-card/product-card-quickadd.tsx"
    }
  ],
  "meta": {
    "section": "product-card",
    "variant": "quickadd",
    "niche": [
      "supplements",
      "wellness"
    ],
    "commerce": [
      "atc",
      "variants",
      "quantity",
      "stock"
    ],
    "previewHeight": 480
  }
}