{
  "name": "cart-drawer",
  "title": "CartDrawer",
  "description": "Slide-in side cart drawer with provider context, free-shipping progress, animated subtotal, and empty state. Auto-opens on add-to-cart.",
  "type": "component",
  "registryDependencies": [
    "cart-summary",
    "price",
    "cn"
  ],
  "files": [
    {
      "path": "cart-drawer.tsx",
      "content": "\"use client\";\n\nimport React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from \"react\";\nimport { CartSummary } from \"./cart-summary\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { useCart } from \"@cimplify/sdk/react\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\ninterface CartDrawerCtx {\n  isOpen: boolean;\n  open: () => void;\n  close: () => void;\n  toggle: () => void;\n}\n\nconst CartDrawerContext = createContext<CartDrawerCtx | null>(null);\n\nexport function useCartDrawer(): CartDrawerCtx {\n  const ctx = useContext(CartDrawerContext);\n  if (!ctx) {\n    throw new Error(\"useCartDrawer must be used within <CartDrawerProvider>\");\n  }\n  return ctx;\n}\n\ninterface ProviderProps {\n  children: React.ReactNode;\n  /** Auto-open the drawer whenever the cart's pendingOpCount goes from 0 → >0. Default: true. */\n  openOnAdd?: boolean;\n}\n\nexport function CartDrawerProvider({ children, openOnAdd = true }: ProviderProps): React.ReactElement {\n  const [isOpen, setIsOpen] = useState(false);\n  const cart = useCart();\n  const lastPendingRef = useRef(0);\n\n  const open = useCallback(() => setIsOpen(true), []);\n  const close = useCallback(() => setIsOpen(false), []);\n  const toggle = useCallback(() => setIsOpen((v) => !v), []);\n\n  useEffect(() => {\n    if (!openOnAdd) return;\n    if (cart.pendingOpCount > lastPendingRef.current && cart.pendingOpCount > 0) {\n      setIsOpen(true);\n    }\n    lastPendingRef.current = cart.pendingOpCount;\n  }, [cart.pendingOpCount, openOnAdd]);\n\n  const value = useMemo<CartDrawerCtx>(() => ({ isOpen, open, close, toggle }), [isOpen, open, close, toggle]);\n  return <CartDrawerContext.Provider value={value}>{children}</CartDrawerContext.Provider>;\n}\n\nexport interface CartDrawerProps {\n  /** Called when \"Checkout\" is clicked. Drawer auto-closes first. */\n  onCheckout?: () => void;\n  /** Called when \"Continue Shopping\" is clicked. Defaults to closing. */\n  onContinueShopping?: () => void;\n  /** Called when the empty state's CTA is clicked. */\n  onShop?: () => void;\n  /** Heading. */\n  title?: string;\n  /** Free-shipping threshold (in business currency, numeric). 0 disables the progress bar. */\n  freeShippingThreshold?: number;\n  /** Custom class on the panel. */\n  className?: string;\n}\n\n/**\n * Animate a number toward `target` over ~250ms. Used for subtotal so it\n * feels alive when items are added/removed.\n */\nfunction useAnimatedNumber(target: number, durationMs = 250) {\n  const [value, setValue] = useState(target);\n  const fromRef = useRef(target);\n  const startRef = useRef<number | null>(null);\n  useEffect(() => {\n    fromRef.current = value;\n    startRef.current = null;\n    let raf = 0;\n    const tick = (t: number) => {\n      if (startRef.current == null) startRef.current = t;\n      const p = Math.min(1, (t - startRef.current) / durationMs);\n      const eased = 1 - Math.pow(1 - p, 3);\n      setValue(fromRef.current + (target - fromRef.current) * eased);\n      if (p < 1) raf = requestAnimationFrame(tick);\n    };\n    raf = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(raf);\n    // value intentionally omitted — only re-trigger on target change\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [target, durationMs]);\n  return value;\n}\n\nexport function CartDrawer({\n  onCheckout,\n  onContinueShopping,\n  onShop,\n  title = \"Cart\",\n  freeShippingThreshold = 0,\n  className,\n}: CartDrawerProps): React.ReactElement {\n  const { isOpen, close } = useCartDrawer();\n  const cart = useCart();\n  const subtotalNum = parsePrice(cart.subtotal);\n  const animatedSubtotal = useAnimatedNumber(subtotalNum);\n\n  // Lock body scroll + close on Escape while open.\n  useEffect(() => {\n    if (!isOpen) return;\n    const original = document.body.style.overflow;\n    document.body.style.overflow = \"hidden\";\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") close();\n    };\n    window.addEventListener(\"keydown\", onKey);\n    return () => {\n      document.body.style.overflow = original;\n      window.removeEventListener(\"keydown\", onKey);\n    };\n  }, [isOpen, close]);\n\n  const handleCheckout = () => {\n    close();\n    onCheckout?.();\n  };\n\n  const handleContinue = () => {\n    onContinueShopping?.();\n    close();\n  };\n\n  const handleShop = () => {\n    onShop?.();\n    close();\n  };\n\n  // Free-shipping progress\n  const showShippingBar = freeShippingThreshold > 0 && !cart.isEmpty;\n  const remainingForShipping = Math.max(0, freeShippingThreshold - subtotalNum);\n  const shippingProgress = freeShippingThreshold > 0 ? Math.min(100, (subtotalNum / freeShippingThreshold) * 100) : 0;\n  const shippingUnlocked = subtotalNum >= freeShippingThreshold && freeShippingThreshold > 0;\n\n  return (\n    <div\n      data-cimplify-cart-drawer\n      data-open={isOpen ? \"true\" : \"false\"}\n      aria-hidden={!isOpen}\n      className={cn(\n        \"fixed inset-0 z-[200]\",\n        isOpen ? \"pointer-events-auto\" : \"pointer-events-none\",\n      )}\n    >\n      {/* Backdrop */}\n      <div\n        onClick={close}\n        className={cn(\n          \"absolute inset-0 bg-foreground/40 backdrop-blur-sm transition-opacity duration-300\",\n          isOpen ? \"opacity-100\" : \"opacity-0\",\n        )}\n      />\n\n      {/* Panel */}\n      <aside\n        role=\"dialog\"\n        aria-modal=\"true\"\n        aria-label={title}\n        className={cn(\n          \"absolute top-0 right-0 h-full w-full sm:max-w-[480px] bg-background shadow-2xl flex flex-col\",\n          \"transition-transform duration-300\",\n          isOpen ? \"translate-x-0\" : \"translate-x-full\",\n          // ease-out cubic-bezier for a \"pull\" feel\n          \"[transition-timing-function:cubic-bezier(0.16,1,0.3,1)]\",\n          className,\n        )}\n      >\n        {/* Header */}\n        <header className=\"relative flex items-center justify-between gap-4 px-6 py-5 shrink-0\">\n          <div className=\"flex items-baseline gap-2\">\n            <h2 className=\"text-xl font-bold tracking-tight m-0\">{title}</h2>\n            {cart.itemCount > 0 && (\n              <span className=\"text-sm text-muted-foreground tabular-nums\">\n                {cart.itemCount} {cart.itemCount === 1 ? \"item\" : \"items\"}\n              </span>\n            )}\n          </div>\n          <button\n            type=\"button\"\n            onClick={close}\n            aria-label=\"Close cart\"\n            className=\"grid place-items-center w-9 h-9 rounded-full hover:bg-muted text-muted-foreground hover:text-foreground transition-colors\"\n          >\n            <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" viewBox=\"0 0 24 24\" aria-hidden>\n              <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n            </svg>\n          </button>\n        </header>\n\n        {/* Free-shipping bar */}\n        {showShippingBar && (\n          <div data-cimplify-cart-shipping-bar className=\"px-6 pb-4 shrink-0\">\n            <div className=\"flex items-baseline justify-between gap-2 mb-1.5 text-xs\">\n              {shippingUnlocked ? (\n                <span className=\"font-medium text-primary\">✓ Free shipping unlocked</span>\n              ) : (\n                <span className=\"text-muted-foreground\">\n                  <Price amount={remainingForShipping} className=\"font-semibold text-foreground\" /> away from free shipping\n                </span>\n              )}\n            </div>\n            <div className=\"h-1 rounded-full bg-muted overflow-hidden\">\n              <div\n                className=\"h-full bg-primary transition-[width] duration-500 ease-out\"\n                style={{ width: `${shippingProgress}%` }}\n              />\n            </div>\n          </div>\n        )}\n\n        {/* Items */}\n        <div className=\"flex-1 overflow-y-auto px-6\">\n          {cart.isEmpty ? (\n            <EmptyState onShop={handleShop} />\n          ) : (\n            <CartSummary showTotals={false} showCheckoutButton={false} />\n          )}\n        </div>\n\n        {/* Footer */}\n        {!cart.isEmpty && (\n          <footer className=\"border-t border-border px-6 py-5 shrink-0 space-y-3 bg-background\">\n            <div className=\"flex items-baseline justify-between\">\n              <span className=\"text-sm text-muted-foreground\">Subtotal</span>\n              <Price amount={animatedSubtotal} className=\"text-lg font-bold tabular-nums\" />\n            </div>\n            <p className=\"text-[11px] text-muted-foreground\">\n              Tax and shipping calculated at checkout.\n            </p>\n            <button\n              type=\"button\"\n              onClick={handleCheckout}\n              className=\"w-full h-12 rounded-full bg-foreground text-background font-semibold text-sm hover:bg-foreground/90 active:scale-[0.99] transition-all\"\n            >\n              Checkout — <Price amount={cart.subtotal} className=\"tabular-nums\" />\n            </button>\n            <button\n              type=\"button\"\n              onClick={handleContinue}\n              className=\"w-full text-xs font-medium text-muted-foreground hover:text-foreground transition-colors\"\n            >\n              Continue shopping\n            </button>\n            <div className=\"flex items-center justify-center gap-1.5 text-[10px] text-muted-foreground/70 uppercase tracking-wider pt-1\">\n              <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" viewBox=\"0 0 24 24\" aria-hidden>\n                <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\" />\n              </svg>\n              <span>Secure checkout</span>\n            </div>\n          </footer>\n        )}\n      </aside>\n    </div>\n  );\n}\n\nfunction EmptyState({ onShop }: { onShop: () => void }) {\n  return (\n    <div data-cimplify-cart-empty className=\"h-full grid place-items-center text-center py-16\">\n      <div className=\"space-y-5 max-w-[260px]\">\n        <div className=\"relative w-20 h-20 mx-auto\">\n          <div className=\"absolute inset-0 rounded-full bg-muted\" />\n          <svg\n            className=\"relative w-10 h-10 m-auto top-1/2 -translate-y-1/2 text-muted-foreground/60\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.5\"\n            viewBox=\"0 0 24 24\"\n            aria-hidden\n          >\n            <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M16 11V7a4 4 0 00-8 0v4M5 9h14l-1 12H6L5 9z\" />\n          </svg>\n        </div>\n        <div>\n          <p className=\"text-base font-semibold m-0\">Your cart is empty</p>\n          <p className=\"text-sm text-muted-foreground mt-1\">\n            Discover something you'll love.\n          </p>\n        </div>\n        <button\n          type=\"button\"\n          onClick={onShop}\n          className=\"inline-flex items-center gap-1.5 h-11 px-6 rounded-full bg-foreground text-background text-sm font-semibold hover:bg-foreground/90 active:scale-[0.99] transition-all\"\n        >\n          Shop now\n          <svg className=\"w-3.5 h-3.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" viewBox=\"0 0 24 24\" aria-hidden>\n            <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M14 5l7 7m0 0l-7 7m7-7H3\" />\n          </svg>\n        </button>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}
