{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cart-sheet",
  "type": "registry:component",
  "title": "Cart bottom sheet",
  "description": "Mobile bottom sheet after add to cart with a drag handle, the added item, cart subtotal, View cart and Checkout. Focus trap, Escape, scroll lock.",
  "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-sheet.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion, type PanInfo } from \"motion/react\"\nimport { Check, 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/* Mobile bottom sheet shown after add to cart. Keeps the shopper on the\n   page with the item, the cart subtotal and two clear next steps. Drag the\n   handle down (or tap the scrim, or press Escape) to dismiss. Pass\n   `contained` to pin it inside a relative parent instead of the viewport. */\n\nexport type CartSheetItem = { title: string; variant?: string; price: number; image: string; quantity?: number }\n\nconst SAMPLE_ITEM: CartSheetItem = { title: \"Daily Greens\", variant: \"30 servings\", price: 4900, image: \"/brands/morrow/greens.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 CartSheet({\n  open,\n  onOpenChange,\n  item = SAMPLE_ITEM,\n  cartCount = 3,\n  cartSubtotal = 19700,\n  contained = false,\n  onViewCart,\n  onCheckout,\n  className,\n}: {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n  item?: CartSheetItem\n  /** Items in the cart after this add. */\n  cartCount?: number\n  /** Cart subtotal in cents after this add. */\n  cartSubtotal?: number\n  contained?: boolean\n  onViewCart?: () => void\n  onCheckout?: () => void\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"cart\", \"cart-sheet\")\n  const panelRef = React.useRef<HTMLDivElement>(null)\n  const primaryRef = React.useRef<HTMLButtonElement>(null)\n  const firstMount = React.useRef(true)\n  const titleId = React.useId()\n  const d = reduce ? 0 : 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  React.useEffect(() => {\n    const skip = firstMount.current && contained\n    firstMount.current = false\n    if (!open || skip) return\n    const opener = document.activeElement as HTMLElement | null\n    const t = window.setTimeout(() => primaryRef.current?.focus({ preventScroll: true }), 30)\n    return () => {\n      window.clearTimeout(t)\n      if (opener && opener !== document.body && opener.isConnected) opener.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  const onDragEnd = (_: unknown, info: PanInfo) => {\n    if (info.offset.y > 96 || info.velocity.y > 500) onOpenChange(false)\n  }\n\n  const qty = item.quantity ?? 1\n\n  return (\n    <AnimatePresence>\n      {open ? (\n        <div key=\"sk-cart-sheet\" ref={sk.ref as React.Ref<HTMLDivElement>} {...sk.attrs} className={cn(contained ? \"absolute\" : \"fixed\", \"inset-0 z-50\", className)}>\n          <motion.div\n            aria-hidden\n            className=\"absolute inset-0 bg-[rgb(0_0_0/0.45)]\"\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            onKeyDown={onPanelKeyDown}\n            drag={reduce ? false : \"y\"}\n            dragConstraints={{ top: 0, bottom: 0 }}\n            dragElastic={{ top: 0, bottom: 0.6 }}\n            onDragEnd={onDragEnd}\n            className=\"absolute inset-x-0 bottom-0 mx-auto w-full max-w-md rounded-t-sk-card border border-b-0 border-sk-line bg-sk-paper px-5 pb-[max(1.5rem,env(safe-area-inset-bottom))] pt-2 shadow-[0_-24px_60px_-24px_rgb(0_0_0/0.4)]\"\n            initial={{ y: \"100%\" }}\n            animate={{ y: 0 }}\n            exit={{ y: \"100%\" }}\n            transition={{ duration: 0.36 * d, ease: [0.32, 0.72, 0, 1] }}\n          >\n            <div className=\"flex justify-center py-2\" aria-hidden>\n              <span className=\"h-1 w-10 cursor-grab rounded-sk-chip bg-sk-ink/20 active:cursor-grabbing\" />\n            </div>\n            <div className=\"flex items-center justify-between gap-3\">\n              <h2 id={titleId} className=\"flex items-center gap-2 text-[15px] font-medium text-sk-ink\">\n                <span className=\"grid h-6 w-6 place-items-center rounded-sk-chip bg-sk-savings/15 text-sk-savings\">\n                  <Check size={14} />\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 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-3 flex items-center gap-3 rounded-sk-card border border-sk-line bg-sk-card p-3\">\n              <ProductImage src={item.image} alt={item.title} className=\"h-16 w-16 shrink-0 rounded-sk-card\" fit=\"cover\" zoomOnHover={false} />\n              <div className=\"min-w-0 flex-1\">\n                <p className=\"line-clamp-2 text-sm font-medium\">{item.title}</p>\n                <p className=\"text-[13px] text-sk-muted\">\n                  {item.variant ? `${item.variant} / ` : null}Qty <span className=\"tabular-nums\">{qty}</span>\n                </p>\n              </div>\n              <p className=\"shrink-0 text-sm font-semibold tabular-nums text-sk-price\">{money(item.price * qty)}</p>\n            </div>\n\n            <div className=\"mt-4 flex items-baseline justify-between text-sm\">\n              <span className=\"text-sk-muted\">\n                Cart subtotal (<span className=\"tabular-nums\">{cartCount}</span> {cartCount === 1 ? \"item\" : \"items\"})\n              </span>\n              <span className=\"font-semibold tabular-nums\">{money(cartSubtotal)}</span>\n            </div>\n\n            <div className=\"mt-4 grid grid-cols-2 gap-2\">\n              <Button\n                variant=\"outline\"\n                size=\"lg\"\n                className=\"w-full px-3 active:scale-[0.97]\"\n                onClick={() => {\n                  sk.emit(\"interact\", \"view_cart\")\n                  onViewCart?.()\n                  onOpenChange(false)\n                }}\n              >\n                View cart\n              </Button>\n              <Button\n                ref={primaryRef}\n                size=\"lg\"\n                className=\"w-full px-3 active:scale-[0.97]\"\n                onClick={() => {\n                  sk.emit(\"convert\", \"checkout\", cartSubtotal)\n                  onCheckout?.()\n                }}\n              >\n                Checkout\n              </Button>\n            </div>\n            <button\n              type=\"button\"\n              onClick={() => onOpenChange(false)}\n              className=\"mt-2 h-11 w-full rounded-sk-btn text-[13px] font-medium text-sk-muted transition-colors duration-200 hover:text-sk-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sk-accent active:scale-[0.97]\"\n            >\n              Keep shopping\n            </button>\n          </motion.div>\n        </div>\n      ) : null}\n    </AnimatePresence>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/storekit/cart/cart-sheet.tsx"
    }
  ],
  "docs": "Controlled by open/onOpenChange. Drag down past 96px to dismiss. Pass contained to pin it inside a relative parent.",
  "meta": {
    "section": "cart",
    "variant": "sheet",
    "niche": [
      "any"
    ],
    "commerce": [
      "cart",
      "atc"
    ],
    "previewHeight": 620
  }
}