{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "product-card-variant",
  "type": "registry:component",
  "title": "Variant select product card",
  "description": "In-card option select that swaps price and crossfades the image, a spring-pop wishlist heart and a rating row.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "product-card",
    "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/product-card-minimal.json",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "product-card/product-card-variant.tsx",
      "content": "\"use client\"\n\nimport { useId, useState } from \"react\"\nimport { AnimatePresence, motion, useAnimate, useReducedMotion } from \"motion/react\"\nimport { Check, ChevronDown, Heart } from \"lucide-react\"\nimport { cn } 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 { useAddState } from \"@/components/storekit/product-card/product-card-minimal\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Variant tile. A native select (full keyboard and screen reader support for\n   free) swaps both price and image; images crossfade. The wishlist heart pops\n   1 to 1.3 to 1 when saved. Rating sits under the title. */\n\nconst EASE = [0.23, 1, 0.32, 1] as const\n\ntype Variant = { label: string; price: number; compareAt?: number; image: string }\n\nexport function ProductCardVariant({\n  title = \"Electrolytes\",\n  href = \"#\",\n  variants = [\n    { label: \"Citrus, 30 sticks\", price: 3200, image: \"/placeholder.webp\" },\n    { label: \"Berry, 30 sticks\", price: 3200, image: \"/placeholder.webp\" },\n  ],\n  rating = 4.8,\n  reviewCount = 214,\n  defaultSaved = false,\n  onSave,\n  onAdd,\n  className,\n}: {\n  title?: string\n  href?: string\n  variants?: Variant[]\n  rating?: number\n  reviewCount?: number\n  defaultSaved?: boolean\n  onSave?: (saved: boolean) => void\n  onAdd?: (line: { title: string; variant: string; price: number }) => void | Promise<void>\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const selectId = useId()\n  const [index, setIndex] = useState(0)\n  const [saved, setSaved] = useState(defaultSaved)\n  const [heart, animateHeart] = useAnimate()\n  const v = variants[index]\n  const sk = useSkBlock(\"product-card\", \"product-card-variant\")\n  const { state, add } = useAddState(() => {\n    sk.emit(\"convert\", \"add_to_cart\", v.price)\n    return onAdd?.({ title, variant: v.label, price: v.price })\n  })\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 aspect-square overflow-hidden rounded-sk-card bg-sk-surface\">\n        <AnimatePresence initial={false}>\n          <motion.img\n            key={v.image}\n            src={v.image}\n            alt=\"\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            transition={{ duration: reduce ? 0 : 0.35, ease: EASE }}\n            className=\"absolute inset-0 h-full w-full object-cover transition-transform duration-700 ease-sk-out group-hover/img:scale-[1.04] motion-reduce:group-hover/img:scale-100\"\n          />\n        </AnimatePresence>\n        <button\n          type=\"button\"\n          aria-pressed={saved}\n          aria-label={saved ? `Remove ${title} from wishlist` : `Save ${title} to wishlist`}\n          onClick={() => {\n            const next = !saved\n            setSaved(next)\n            if (next && !reduce) animateHeart(heart.current, { scale: [1, 1.3, 1] }, { duration: 0.35, ease: EASE, times: [0, 0.4, 1] })\n            onSave?.(next)\n          }}\n          className=\"absolute right-3 top-3 z-10 grid h-11 w-11 place-items-center rounded-full border border-sk-line bg-sk-card text-sk-ink outline-none transition-transform duration-200 ease-sk-out focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper active:scale-[0.97]\"\n        >\n          <span ref={heart} className=\"grid place-items-center\">\n            <Heart size={18} aria-hidden className={cn(\"transition-colors duration-200\", saved ? \"fill-sk-sale text-sk-sale\" : \"fill-transparent\")} />\n          </span>\n        </button>\n      </div>\n\n      <div className=\"flex flex-col gap-1.5\">\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          </h3>\n          <span className=\"relative inline-grid overflow-hidden\" aria-live=\"polite\">\n            <AnimatePresence mode=\"popLayout\" initial={false}>\n              <motion.span\n                key={v.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={v.price} compareAt={v.compareAt} size=\"sm\" />\n              </motion.span>\n            </AnimatePresence>\n          </span>\n        </div>\n        <Stars value={rating} count={reviewCount} size={12} />\n      </div>\n\n      <div className=\"relative z-10 flex flex-col gap-2\">\n        <label htmlFor={selectId} className=\"sr-only\">\n          Option\n        </label>\n        <div className=\"relative\">\n          <select\n            id={selectId}\n            value={index}\n            onChange={(e) => setIndex(Number(e.target.value))}\n            className=\"h-11 w-full appearance-none rounded-sk-btn border border-sk-line bg-sk-card pl-3.5 pr-10 text-base text-sk-ink outline-none sm:text-sm transition-[border-color] duration-200 hover:border-sk-ink/30 focus-visible:border-sk-accent focus-visible:ring-[3px] focus-visible:ring-sk-accent/15\"\n          >\n            {variants.map((opt, i) => (\n              <option key={opt.label} value={i}>\n                {opt.label}\n              </option>\n            ))}\n          </select>\n          <ChevronDown size={16} aria-hidden className=\"pointer-events-none absolute right-3.5 top-1/2 -translate-y-1/2 text-sk-muted\" />\n        </div>\n        <Button className=\"w-full active:scale-[0.97]\" loading={state === \"loading\"} onClick={add} aria-label={`Add ${title}, ${v.label}, to cart`}>\n          {state === \"added\" ? (<><Check size={16} aria-hidden />Added</>) : \"Add to cart\"}\n        </Button>\n      </div>\n    </article>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/storekit/product-card/product-card-variant.tsx"
    }
  ],
  "meta": {
    "section": "product-card",
    "variant": "variant",
    "niche": [
      "any"
    ],
    "commerce": [
      "atc",
      "variants",
      "wishlist"
    ],
    "previewHeight": 540
  }
}