{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cart-samples",
  "type": "registry:component",
  "title": "Free sample chooser",
  "description": "Grid of free samples; choose up to two, the rest dim at the limit and say why.",
  "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-samples.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { Check } from \"lucide-react\"\nimport { cn } from \"@/components/storekit/lib/cn\"\nimport { ProductImage } from \"@/components/storekit/ui/product-image\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Free sample chooser. A grid of sample tiles, pick up to `max`. Once the\n   limit is reached the rest dim and say why, and deselecting frees a slot.\n   The count is announced as it changes. */\n\nexport type CartSample = { id: string; title: string; detail?: string; image: string }\n\nconst SAMPLE: CartSample[] = [\n  { id: \"greens-stick\", title: \"Daily Greens\", detail: \"1 stick\", image: \"/brands/morrow/greens.webp\" },\n  { id: \"protein-stick\", title: \"Protein\", detail: \"1 serving\", image: \"/brands/morrow/protein.webp\" },\n  { id: \"focus-trial\", title: \"Focus\", detail: \"2 capsules\", image: \"/brands/morrow/focus.webp\" },\n  { id: \"magnesium-trial\", title: \"Magnesium Night\", detail: \"3 gummies\", image: \"/brands/morrow/magnesium.webp\" },\n]\n\nexport function CartSamples({\n  samples = SAMPLE,\n  max = 2,\n  defaultSelected = [],\n  onChange,\n  className,\n}: {\n  samples?: CartSample[]\n  max?: number\n  defaultSelected?: string[]\n  onChange?: (ids: string[]) => void\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"cart\", \"cart-samples\")\n  const [picked, setPicked] = React.useState<string[]>(defaultSelected.slice(0, max))\n  const full = picked.length >= max\n  const titleId = React.useId()\n\n  const toggle = (id: string) => {\n    const on = picked.includes(id)\n    if (!on && full) return\n    const next = on ? picked.filter((x) => x !== id) : [...picked, id]\n    setPicked(next)\n    onChange?.(next)\n    sk.emit(\"interact\", on ? \"sample_remove\" : \"sample_add\")\n  }\n\n  return (\n    <section ref={sk.ref as React.Ref<HTMLElement>} {...sk.attrs} aria-labelledby={titleId} className={cn(\"w-full\", className)}>\n      <div className=\"flex items-baseline justify-between gap-3\">\n        <h3 id={titleId} className=\"text-sm font-medium text-sk-ink\">\n          Choose <span className=\"tabular-nums\">{max}</span> free samples\n        </h3>\n        <p className={cn(\"shrink-0 whitespace-nowrap text-[13px] tabular-nums\", full ? \"text-sk-savings\" : \"text-sk-muted\")} aria-live=\"polite\">\n          {picked.length} of {max} chosen\n        </p>\n      </div>\n      <ul className=\"mt-3 grid grid-cols-2 gap-2 sm:grid-cols-4 sm:gap-3\">\n        {samples.map((s) => {\n          const on = picked.includes(s.id)\n          const locked = !on && full\n          return (\n            <li key={s.id} className=\"min-w-0\">\n              <button\n                type=\"button\"\n                aria-pressed={on}\n                aria-disabled={locked}\n                onClick={() => toggle(s.id)}\n                title={locked ? `You can choose ${max}. Remove one to swap.` : undefined}\n                className={cn(\n                  \"relative flex w-full flex-col rounded-sk-card border p-2 text-left transition-[border-color,background-color,opacity,transform] duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper\",\n                  on ? \"border-sk-accent bg-sk-accent/[0.06]\" : \"border-sk-line bg-sk-card\",\n                  locked ? \"cursor-not-allowed opacity-50\" : \"hover:border-sk-ink/30 active:scale-[0.97]\"\n                )}\n              >\n                <ProductImage src={s.image} alt=\"\" className=\"w-full rounded-sk-card\" zoomOnHover={false} />\n                <span className=\"mt-2 truncate text-[13px] font-medium text-sk-ink\">{s.title}</span>\n                <span className=\"text-xs text-sk-muted\">{s.detail ? `${s.detail} / Free` : \"Free\"}</span>\n                <motion.span\n                  aria-hidden\n                  initial={false}\n                  animate={{ scale: on ? 1 : 0.85, opacity: on ? 1 : 0.9 }}\n                  transition={{ duration: reduce ? 0 : 0.2 }}\n                  className={cn(\n                    \"absolute right-2 top-2 grid h-6 w-6 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={13} strokeWidth={3} /> : null}\n                </motion.span>\n              </button>\n            </li>\n          )\n        })}\n      </ul>\n      <p className=\"mt-2 text-xs text-sk-muted\">{full ? \"Remove one to swap it for another.\" : \"Samples ship with this order at no charge.\"}</p>\n    </section>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/storekit/cart/cart-samples.tsx"
    }
  ],
  "meta": {
    "section": "cart",
    "variant": "grid",
    "niche": [
      "any"
    ],
    "commerce": [
      "samples",
      "gift"
    ],
    "previewHeight": 380
  }
}