{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "checkout-summary",
  "type": "registry:component",
  "title": "Order summary",
  "description": "Checkout sidebar card with quantity-badged thumbs, an expandable discount code and a display-face total.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "checkout",
    "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/input.json",
    "https://storekit-eight.vercel.app/r/product-image.json"
  ],
  "files": [
    {
      "path": "checkout/checkout-summary.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { ChevronDown, Lock } from \"lucide-react\"\nimport { cn, money } from \"@/components/storekit/lib/cn\"\nimport { Button } from \"@/components/storekit/ui/button\"\nimport { Input } from \"@/components/storekit/ui/input\"\nimport { ProductImage } from \"@/components/storekit/ui/product-image\"\n\n/* Order summary card for the checkout sidebar: lines with a quantity badge on\n   the thumb, an expandable discount code, and subtotal, shipping, tax and a\n   display-face total. */\n\ntype SummaryLine = { id: string; title: string; variant?: string; price: number; image: string; quantity: number }\n\nconst SAMPLE: SummaryLine[] = [\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  { id: \"magnesium-night\", title: \"Magnesium Night\", variant: \"60 gummies\", price: 2900, image: \"/brands/morrow/magnesium.webp\", quantity: 1 },\n]\n\nexport function CheckoutSummary({\n  items = SAMPLE,\n  freeShippingThreshold = 7500,\n  shippingCost = 800,\n  taxRate = 0.08,\n  discountCodes = { WELCOME10: 10 },\n  className,\n}: {\n  items?: SummaryLine[]\n  /** In cents. */\n  freeShippingThreshold?: number\n  /** In cents, charged below the threshold. */\n  shippingCost?: number\n  taxRate?: number\n  /** Code to percent off. */\n  discountCodes?: Record<string, number>\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const [open, setOpen] = React.useState(false)\n  const [code, setCode] = React.useState(\"\")\n  const [error, setError] = React.useState<string>()\n  const [applied, setApplied] = React.useState<{ code: string; pct: number } | null>(null)\n  const regionId = React.useId()\n\n  const subtotal = items.reduce((n, l) => n + l.price * l.quantity, 0)\n  const discount = applied ? Math.round((subtotal * applied.pct) / 100) : 0\n  const net = subtotal - discount\n  const shipping = net >= freeShippingThreshold ? 0 : shippingCost\n  const tax = Math.round(net * taxRate)\n  const total = net + shipping + tax\n\n  const apply = (e: React.FormEvent) => {\n    e.preventDefault()\n    const key = code.trim().toUpperCase()\n    const pct = discountCodes[key]\n    if (!key) return setError(\"Enter a code.\")\n    if (!pct) return setError(\"That code is not valid.\")\n    setApplied({ code: key, pct })\n    setError(undefined)\n    setCode(\"\")\n  }\n\n  return (\n    <section aria-label=\"Order summary\" data-sk-section=\"checkout\" data-sk-variant=\"checkout-summary\" className={cn(\"w-full rounded-sk-card border border-sk-line bg-sk-card p-5 sm:p-6\", className)}>\n      <p className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">Order summary</p>\n      <ul className=\"mt-4 grid grid-cols-1 gap-4\">\n        {items.map((l) => (\n          <li key={l.id} className=\"flex items-center gap-3\">\n            <div className=\"relative shrink-0\">\n              <ProductImage src={l.image} alt={l.title} className=\"h-16 w-16 rounded-sk-card border border-sk-line\" fit=\"cover\" zoomOnHover={false} />\n              <span className=\"absolute -right-2 -top-2 grid h-5 min-w-5 place-items-center rounded-full bg-sk-ink px-1.5 text-[11px] font-semibold tabular-nums leading-none text-sk-paper\">\n                <span className=\"sr-only\">Quantity </span>\n                {l.quantity}\n              </span>\n            </div>\n            <div className=\"min-w-0 flex-1\">\n              <p className=\"line-clamp-2 break-words text-sm font-medium\">{l.title}</p>\n              {l.variant ? <p className=\"text-sm text-sk-muted\">{l.variant}</p> : null}\n            </div>\n            <span className=\"shrink-0 text-sm font-medium tabular-nums text-sk-price\">{money(l.price * l.quantity)}</span>\n          </li>\n        ))}\n      </ul>\n\n      <div className=\"mt-5 border-y border-sk-line\">\n        <button\n          type=\"button\"\n          aria-expanded={open}\n          aria-controls={regionId}\n          onClick={() => setOpen((o) => !o)}\n          className=\"flex h-12 w-full items-center justify-between text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sk-accent\"\n        >\n          {applied ? `Code ${applied.code} applied` : \"Discount code\"}\n          <ChevronDown size={16} className={cn(\"text-sk-muted transition-transform duration-200\", open && \"rotate-180\")} />\n        </button>\n        <AnimatePresence initial={false}>\n          {open ? (\n            <motion.div\n              id={regionId}\n              initial={{ height: 0, opacity: 0 }}\n              animate={{ height: \"auto\", opacity: 1 }}\n              exit={{ height: 0, opacity: 0 }}\n              transition={{ duration: reduce ? 0 : 0.28, ease: [0.16, 1, 0.3, 1] }}\n              className=\"overflow-hidden\"\n            >\n              <form onSubmit={apply} className=\"flex items-start gap-2 px-px pb-4\">\n                <div className=\"flex-1\">\n                  <Input aria-label=\"Discount code\" placeholder=\"WELCOME10\" value={code} onChange={(e) => setCode(e.target.value)} error={error} />\n                </div>\n                <Button type=\"submit\" variant=\"outline\">Apply</Button>\n              </form>\n            </motion.div>\n          ) : null}\n        </AnimatePresence>\n      </div>\n\n      <dl className=\"mt-4 grid gap-2 text-sm\">\n        <div className=\"flex justify-between\">\n          <dt className=\"text-sk-muted\">Subtotal</dt>\n          <dd className=\"tabular-nums\">{money(subtotal)}</dd>\n        </div>\n        {applied ? (\n          <div className=\"flex justify-between text-sk-savings\">\n            <dt>Discount ({applied.pct}%)</dt>\n            <dd className=\"tabular-nums\">-{money(discount)}</dd>\n          </div>\n        ) : null}\n        <div className=\"flex justify-between\">\n          <dt className=\"text-sk-muted\">Shipping</dt>\n          <dd className={cn(\"tabular-nums\", shipping === 0 && \"font-medium text-sk-savings\")}>{shipping === 0 ? \"Free\" : money(shipping)}</dd>\n        </div>\n        <div className=\"flex justify-between\">\n          <dt className=\"text-sk-muted\">Estimated tax</dt>\n          <dd className=\"tabular-nums\">{money(tax)}</dd>\n        </div>\n        <div className=\"mt-2 flex items-baseline justify-between border-t border-sk-line pt-4\">\n          <dt className=\"font-medium\">Total</dt>\n          <dd className=\"font-sk-display text-2xl font-semibold tabular-nums tracking-[-0.02em] text-sk-price\">{money(total)}</dd>\n        </div>\n      </dl>\n\n      <p className=\"mt-5 flex items-center justify-center gap-2 font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">\n        <Lock size={12} />\n        Secure checkout\n      </p>\n    </section>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/storekit/checkout/checkout-summary.tsx"
    }
  ],
  "meta": {
    "section": "checkout",
    "variant": "summary",
    "niche": [
      "any"
    ],
    "commerce": [
      "order-summary",
      "discount",
      "shipping-threshold",
      "tax"
    ],
    "previewHeight": 560
  }
}