{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "product-card-minimal",
  "type": "registry:component",
  "title": "Minimal product card",
  "description": "One-link tile with synchronized hover zoom, category eyebrow, a two-line title, price and an always-visible Add on the image with loading and Added states.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "product-card",
    "any"
  ],
  "dependencies": [
    "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/product-image.json",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "product-card/product-card-minimal.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { Check, Plus } 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 { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Minimal product tile. The whole card is one link (stretched title link)\n   and hover is synchronized across it: the image zooms whenever the card is\n   hovered. The render fills the tile edge to edge, and an Add button sits on\n   the image corner at every width, so a static card always shows price and\n   an add affordance. The title gets the full width and clamps to two lines.\n   At most one badge: stock, then sale, then new. */\n\nexport type MinimalProduct = {\n  title: string\n  price: number\n  compareAt?: number\n  image: string\n  href?: string\n  category?: string\n  stock?: \"in\" | \"low\" | \"out\"\n  isNew?: boolean\n}\n\nexport function useAddState(onAdd?: () => void | Promise<void>) {\n  const [state, setState] = useState<\"idle\" | \"loading\" | \"added\">(\"idle\")\n  const timer = useRef<ReturnType<typeof setTimeout>>(undefined)\n  useEffect(() => () => clearTimeout(timer.current), [])\n  const add = async () => {\n    if (state !== \"idle\") return\n    setState(\"loading\")\n    await Promise.all([onAdd?.(), new Promise((r) => setTimeout(r, 450))])\n    setState(\"added\")\n    timer.current = setTimeout(() => setState(\"idle\"), 1200)\n  }\n  return { state, add }\n}\n\nfunction tileBadge(p: MinimalProduct) {\n  if (p.stock === \"out\") return <Badge tone=\"outline\">Sold out</Badge>\n  if (p.stock === \"low\") return <Badge tone=\"sale\">Low stock</Badge>\n  if (typeof p.compareAt === \"number\" && p.compareAt > p.price) return <Badge tone=\"sale\">Sale</Badge>\n  if (p.isNew) return <Badge tone=\"accent\">New</Badge>\n  return null\n}\n\nexport function ProductCardMinimal({\n  product = { title: \"Daily Greens\", price: 4900, image: \"/placeholder.webp\", category: \"Powders\" },\n  onAdd,\n  className,\n}: {\n  product?: MinimalProduct\n  onAdd?: (product: MinimalProduct) => void | Promise<void>\n  className?: string\n}) {\n  const sk = useSkBlock(\"product-card\", \"product-card-minimal\")\n  const { state, add } = useAddState(() => {\n    sk.emit(\"convert\", \"add_to_cart\", product.price)\n    return onAdd?.(product)\n  })\n  const soldOut = product.stock === \"out\"\n  const badge = tileBadge(product)\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={product.image} alt=\"\" fit=\"cover\" className=\"rounded-sk-card\" />\n        {badge ? <div className=\"absolute left-3 top-3 overflow-hidden rounded-sk-chip bg-sk-card\">{badge}</div> : null}\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          loading={state === \"loading\"}\n          disabled={soldOut}\n          onClick={add}\n          aria-label={state === \"added\" ? `${product.title} added to cart` : `Add ${product.title} to cart`}\n          className={cn(\n            \"absolute bottom-3 right-3 z-10 h-11 w-11 border-sk-line bg-sk-card p-0 hover:bg-sk-card text-sk-ink active:scale-[0.97] lg:h-9 lg:w-auto lg:px-3\",\n            \"transition-[background-color,color,border-color,transform] duration-200 ease-sk-out hover:border-sk-ink lg:group-hover/img:border-sk-ink\",\n            state === \"added\" && \"border-sk-accent bg-sk-accent text-sk-accent-ink\"\n          )}\n        >\n          {state === \"added\" ? <Check size={16} aria-hidden /> : <Plus size={16} aria-hidden />}\n          <span className=\"hidden lg:inline\">{state === \"added\" ? \"Added\" : \"Add\"}</span>\n        </Button>\n      </div>\n      <div className=\"min-w-0\">\n        {product.category ? (\n          <p className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">{product.category}</p>\n        ) : null}\n        <h3 className=\"mt-1 line-clamp-2 text-[15px] font-medium leading-[1.3] text-sk-ink\">\n          <a\n            href={product.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            {product.title}\n          </a>\n        </h3>\n        <Price amount={product.price} compareAt={product.compareAt} size=\"sm\" className=\"mt-1\" />\n      </div>\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {state === \"added\" ? `${product.title} added to cart` : \"\"}\n      </span>\n    </article>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/storekit/product-card/product-card-minimal.tsx"
    }
  ],
  "meta": {
    "section": "product-card",
    "variant": "minimal",
    "niche": [
      "any"
    ],
    "commerce": [
      "atc"
    ],
    "previewHeight": 440
  }
}