{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "atc-modal",
  "type": "registry:component",
  "title": "Add to cart modal",
  "description": "Centered confirmation with the item, cart subtotal, Continue shopping and Checkout. Focus trap, Escape, scroll lock, focus return.",
  "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/atc-modal.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { Check, Lock, X } 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/* Centered add-to-cart confirmation. Shows the item, the cart subtotal and\n   two choices: keep shopping or go to checkout. Focus is trapped inside,\n   Escape and the scrim close it, and the page scroll locks while open.\n   Pass `contained` to pin it inside a relative parent. */\n\nexport type AtcModalItem = { title: string; variant?: string; price: number; compareAt?: number; image: string; quantity?: number }\n\nconst SAMPLE_ITEM: AtcModalItem = { title: \"Magnesium Night\", variant: \"60 gummies\", price: 2900, image: \"/brands/morrow/magnesium.webp\", quantity: 1 }\n\nconst FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex=\"-1\"])'\n\nexport function AtcModal({\n  open,\n  onOpenChange,\n  item = SAMPLE_ITEM,\n  cartCount = 2,\n  cartSubtotal = 14800,\n  contained = false,\n  onCheckout,\n  className,\n}: {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n  item?: AtcModalItem\n  cartCount?: number\n  /** In cents, after this add. */\n  cartSubtotal?: number\n  contained?: boolean\n  onCheckout?: () => void\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"cart\", \"atc-modal\")\n  const panelRef = React.useRef<HTMLDivElement>(null)\n  const continueRef = React.useRef<HTMLButtonElement>(null)\n  const restoreRef = React.useRef<HTMLElement | null>(null)\n  const firstMount = React.useRef(true)\n  const titleId = React.useId()\n  const descId = React.useId()\n  const [busy, setBusy] = React.useState(false)\n  const d = reduce ? 0 : 1\n  const qty = item.quantity ?? 1\n\n  React.useEffect(() => {\n    if (!open || contained) return\n    const prev = document.body.style.overflow\n    document.body.style.overflow = \"hidden\"\n    return () => {\n      document.body.style.overflow = prev\n    }\n  }, [open, contained])\n\n  // Focus in on open and give focus back to the trigger on close.\n  React.useEffect(() => {\n    const skip = firstMount.current && contained\n    firstMount.current = false\n    if (!open || skip) return\n    restoreRef.current = document.activeElement as HTMLElement | null\n    const t = window.setTimeout(() => continueRef.current?.focus({ preventScroll: true }), 30)\n    return () => {\n      window.clearTimeout(t)\n      restoreRef.current?.focus?.({ preventScroll: true })\n    }\n  }, [open, contained])\n\n  React.useEffect(() => {\n    if (!open) return\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") onOpenChange(false)\n    }\n    document.addEventListener(\"keydown\", onKey)\n    return () => document.removeEventListener(\"keydown\", onKey)\n  }, [open, onOpenChange])\n\n  const onPanelKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key !== \"Tab\" || !panelRef.current) return\n    const nodes = Array.from(panelRef.current.querySelectorAll<HTMLElement>(FOCUSABLE))\n    if (!nodes.length) return\n    const first = nodes[0]\n    const last = nodes[nodes.length - 1]\n    if (e.shiftKey && document.activeElement === first) {\n      e.preventDefault()\n      last.focus()\n    } else if (!e.shiftKey && document.activeElement === last) {\n      e.preventDefault()\n      first.focus()\n    }\n  }\n\n  return (\n    <AnimatePresence>\n      {open ? (\n        <div key=\"sk-atc-modal\" ref={sk.ref as React.Ref<HTMLDivElement>} {...sk.attrs} className={cn(contained ? \"absolute\" : \"fixed\", \"inset-0 z-50 grid place-items-center p-4\", className)}>\n          <motion.div\n            aria-hidden\n            className=\"absolute inset-0 bg-[rgb(0_0_0/0.5)]\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            transition={{ duration: 0.2 * d }}\n            onClick={() => onOpenChange(false)}\n          />\n          <motion.div\n            ref={panelRef}\n            role=\"dialog\"\n            aria-modal=\"true\"\n            aria-labelledby={titleId}\n            aria-describedby={descId}\n            onKeyDown={onPanelKeyDown}\n            className=\"relative w-full max-w-md rounded-sk-card border border-sk-line bg-sk-paper p-5 shadow-[0_24px_60px_-24px_rgb(0_0_0/0.5)] sm:p-6\"\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 8, scale: 0.98 }}\n            animate={{ opacity: 1, y: 0, scale: 1 }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, y: 8, scale: 0.98 }}\n            transition={{ duration: 0.3 * d, ease: [0.16, 1, 0.3, 1] }}\n          >\n            <div className=\"flex items-start justify-between gap-3\">\n              <h2 id={titleId} className=\"flex items-center gap-2 font-sk-display text-lg font-semibold tracking-[-0.02em]\">\n                <span className=\"grid h-7 w-7 place-items-center rounded-sk-chip bg-sk-savings/15 text-sk-savings\">\n                  <Check size={15} />\n                </span>\n                Added to cart\n              </h2>\n              <button\n                type=\"button\"\n                aria-label=\"Close\"\n                onClick={() => onOpenChange(false)}\n                className=\"-mr-2 -mt-2 grid h-11 w-11 place-items-center rounded-sk-btn text-sk-muted transition-colors duration-200 hover:bg-sk-ink/[0.05] hover:text-sk-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sk-accent active:scale-[0.97]\"\n              >\n                <X size={18} />\n              </button>\n            </div>\n\n            <div className=\"mt-4 flex items-center gap-4\">\n              <ProductImage src={item.image} alt={item.title} className=\"h-20 w-20 shrink-0 rounded-sk-card border border-sk-line\" fit=\"cover\" zoomOnHover={false} />\n              <div className=\"min-w-0 flex-1\">\n                <p className=\"truncate text-[15px] font-medium\">{item.title}</p>\n                <p className=\"text-sm text-sk-muted\">\n                  {item.variant ? `${item.variant} / ` : null}Qty <span className=\"tabular-nums\">{qty}</span>\n                </p>\n                <p className=\"mt-1 flex items-baseline gap-2 text-sm tabular-nums\">\n                  <span className=\"font-semibold text-sk-price\">{money(item.price * qty)}</span>\n                  {item.compareAt && item.compareAt > item.price ? <s className=\"text-[13px] text-sk-muted\">{money(item.compareAt * qty)}</s> : null}\n                </p>\n              </div>\n            </div>\n\n            <dl id={descId} className=\"mt-5 flex items-baseline justify-between border-t border-sk-line pt-4 text-sm\">\n              <dt className=\"text-sk-muted\">\n                Cart subtotal, <span className=\"tabular-nums\">{cartCount}</span> {cartCount === 1 ? \"item\" : \"items\"}\n              </dt>\n              <dd className=\"font-semibold tabular-nums\">{money(cartSubtotal)}</dd>\n            </dl>\n            <p className=\"mt-1 text-xs text-sk-muted\">Shipping and tax are calculated at checkout.</p>\n\n            <div className=\"mt-5 grid gap-2 sm:grid-cols-2\">\n              <Button ref={continueRef} variant=\"outline\" size=\"lg\" className=\"w-full px-4 active:scale-[0.97]\" onClick={() => onOpenChange(false)}>\n                Continue shopping\n              </Button>\n              <Button\n                size=\"lg\"\n                className=\"w-full px-4 active:scale-[0.97]\"\n                loading={busy}\n                onClick={() => {\n                  sk.emit(\"convert\", \"checkout\", cartSubtotal)\n                  setBusy(true)\n                  window.setTimeout(() => {\n                    setBusy(false)\n                    onCheckout?.()\n                  }, 900)\n                }}\n              >\n                <Lock size={14} />\n                Checkout\n              </Button>\n            </div>\n          </motion.div>\n        </div>\n      ) : null}\n    </AnimatePresence>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/storekit/cart/atc-modal.tsx"
    }
  ],
  "meta": {
    "section": "cart",
    "variant": "modal",
    "niche": [
      "any"
    ],
    "commerce": [
      "atc",
      "cart"
    ],
    "previewHeight": 540
  }
}