{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cart-mini",
  "type": "registry:component",
  "title": "Mini cart",
  "description": "Header cart icon with a count badge and a popover preview with View cart and Checkout.",
  "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/button.json",
    "https://storekit-eight.vercel.app/r/product-image.json",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "cart/cart-mini.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { ShoppingBag } from \"lucide-react\"\nimport { cn, money } from \"@/components/storekit/lib/cn\"\nimport { Button } from \"@/components/storekit/ui/button\"\nimport { ProductImage } from \"@/components/storekit/ui/product-image\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Header cart icon with a count badge. Opens a small popover that grows out\n   of the trigger (scale 0.95 to 1, origin top right). Outside click and\n   Escape close it and return focus to the trigger. */\n\ntype MiniLine = { id: string; title: string; variant?: string; price: number; image: string; quantity: number }\n\nconst SAMPLE: MiniLine[] = [\n  { id: \"daily-greens\", title: \"Daily Greens\", variant: \"30 servings\", price: 4900, image: \"/brands/morrow/greens.webp\", quantity: 2 },\n  { id: \"electrolytes\", title: \"Electrolytes\", variant: \"30 sticks\", price: 3200, image: \"/brands/morrow/electrolytes.webp\", quantity: 1 },\n]\n\nexport function CartMini({\n  items = SAMPLE,\n  defaultOpen = false,\n  onViewCart,\n  onCheckout,\n  className,\n  panelClassName,\n}: {\n  items?: MiniLine[]\n  defaultOpen?: boolean\n  onViewCart?: () => void\n  onCheckout?: () => void\n  className?: string\n  /** Extra classes for the popover, e.g. to span a narrow header on mobile. */\n  panelClassName?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"cart\", \"cart-mini\")\n  const [open, setOpen] = React.useState(defaultOpen)\n  const triggerRef = React.useRef<HTMLButtonElement>(null)\n  const panelId = React.useId()\n  const count = items.reduce((n, l) => n + l.quantity, 0)\n  const subtotal = items.reduce((n, l) => n + l.price * l.quantity, 0)\n\n  React.useEffect(() => {\n    if (!open) return\n    const onDown = (e: PointerEvent) => {\n      if (sk.ref.current && !sk.ref.current.contains(e.target as Node)) setOpen(false)\n    }\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") {\n        setOpen(false)\n        triggerRef.current?.focus()\n      }\n    }\n    document.addEventListener(\"pointerdown\", onDown)\n    document.addEventListener(\"keydown\", onKey)\n    return () => {\n      document.removeEventListener(\"pointerdown\", onDown)\n      document.removeEventListener(\"keydown\", onKey)\n    }\n  }, [open, sk.ref])\n\n  return (\n    <div ref={sk.ref as React.Ref<HTMLDivElement>} {...sk.attrs} className={cn(\"relative inline-block\", className)}>\n      <button\n        ref={triggerRef}\n        type=\"button\"\n        aria-label={`Cart, ${count} items`}\n        aria-expanded={open}\n        aria-controls={panelId}\n        aria-haspopup=\"dialog\"\n        onClick={() => setOpen((o) => !o)}\n        className={cn(\n          \"relative grid h-11 w-11 place-items-center rounded-sk-btn text-sk-ink transition-[transform,background-color] duration-200 hover:bg-sk-ink/[0.05] active:scale-[0.97]\",\n          \"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          open && \"bg-sk-ink/[0.05]\"\n        )}\n      >\n        <ShoppingBag size={20} strokeWidth={1.75} />\n        {count > 0 ? (\n          <span className=\"absolute right-1 top-1 grid h-[18px] min-w-[18px] place-items-center rounded-full bg-sk-accent px-1 text-[11px] font-semibold tabular-nums leading-none text-sk-accent-ink\">\n            {count}\n          </span>\n        ) : null}\n      </button>\n\n      <AnimatePresence>\n        {open ? (\n          <motion.div\n            id={panelId}\n            role=\"dialog\"\n            aria-label=\"Cart preview\"\n            initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.95 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.95 }}\n            transition={{ duration: 0.18, ease: [0.16, 1, 0.3, 1] }}\n            style={{ transformOrigin: \"top right\" }}\n            className={cn(\"absolute right-0 top-full z-40 mt-2 w-[min(320px,calc(100vw-32px))] rounded-sk-card border border-sk-line bg-sk-card p-4 text-left shadow-[0_24px_60px_-24px_rgb(var(--sk-ink)/0.25)]\", panelClassName)}\n          >\n            <div className=\"flex items-baseline justify-between\">\n              <p className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">In your cart</p>\n              <p className=\"text-[13px] tabular-nums text-sk-muted\">{count} items</p>\n            </div>\n            <ul className=\"mt-3 divide-y divide-sk-line\">\n              {items.slice(0, 2).map((l) => (\n                <li key={l.id} className=\"flex items-center gap-3 py-2.5\">\n                  <ProductImage src={l.image} alt={l.title} className=\"h-12 w-12 shrink-0 rounded-sk-chip\" fit=\"cover\" zoomOnHover={false} />\n                  <div className=\"min-w-0 flex-1\">\n                    <p className=\"truncate text-sm font-medium\">{l.title}</p>\n                    <p className=\"text-[13px] text-sk-muted\">\n                      {l.variant ? `${l.variant} / ` : null}Qty {l.quantity}\n                    </p>\n                  </div>\n                  <span className=\"text-sm font-medium tabular-nums text-sk-price\">{money(l.price * l.quantity)}</span>\n                </li>\n              ))}\n            </ul>\n            <div className=\"mt-2 flex justify-between border-t border-sk-line pt-3 text-sm\">\n              <span className=\"text-sk-muted\">Subtotal</span>\n              <span className=\"font-semibold tabular-nums\">{money(subtotal)}</span>\n            </div>\n            <div className=\"mt-3 grid grid-cols-2 gap-2\">\n              <Button variant=\"outline\" size=\"sm\" className=\"h-11\" onClick={onViewCart}>View cart</Button>\n              <Button\n                size=\"sm\"\n                className=\"h-11\"\n                onClick={() => {\n                  sk.emit(\"convert\", \"checkout\", subtotal)\n                  onCheckout?.()\n                }}\n              >\n                Checkout\n              </Button>\n            </div>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/storekit/cart/cart-mini.tsx"
    }
  ],
  "meta": {
    "section": "cart",
    "variant": "popover",
    "niche": [
      "any"
    ],
    "commerce": [
      "cart"
    ],
    "previewHeight": 420
  }
}