{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "checkout-layout",
  "type": "registry:block",
  "title": "Single-page checkout",
  "description": "Numbered Contact, Address, Method and Payment sections that collapse to summaries with Edit, beside a sticky order summary.",
  "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",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "checkout/checkout-layout.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { Check, 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\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Single-page checkout shell. Four numbered sections; the current one is\n   open, finished ones collapse to a one-line summary with an Edit link. The\n   order summary sits in a sticky right column on desktop. Replace the\n   section bodies with your real forms. */\n\ntype Line = { id: string; title: string; variant?: string; price: number; image: string; quantity: number }\n\nconst SAMPLE: Line[] = [\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\nconst METHODS = [\n  { id: \"standard\", label: \"Standard\", note: \"3 to 5 business days\", price: 0 },\n  { id: \"express\", label: \"Express\", note: \"1 to 2 business days\", price: 1200 },\n  { id: \"overnight\", label: \"Overnight\", note: \"Next business day by 10am\", price: 2800 },\n]\n\nconst STEPS = [\"Contact\", \"Shipping address\", \"Shipping method\", \"Payment\"] as const\n\ntype Form = { email: string; name: string; address: string; city: string; zip: string; method: string; card: string; exp: string; cvc: string }\n\nconst DEFAULTS: Form = {\n  email: \"m.rahman@example.com\",\n  name: \"Maya Rahman\",\n  address: \"12 Elmwood Avenue\",\n  city: \"Buffalo, NY\",\n  zip: \"14201\",\n  method: \"standard\",\n  card: \"4242 4242 4242 4242\",\n  exp: \"08 / 28\",\n  cvc: \"123\",\n}\n\nexport function CheckoutLayout({\n  items = SAMPLE,\n  brandName = \"Morrow\",\n  taxRate = 0.08,\n  defaultValues = DEFAULTS,\n  onPlaceOrder,\n  className,\n}: {\n  items?: Line[]\n  brandName?: string\n  taxRate?: number\n  defaultValues?: Form\n  onPlaceOrder?: (form: Form) => void\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"checkout\", \"checkout-layout\")\n  const [step, setStep] = React.useState(1)\n  const [form, setForm] = React.useState<Form>(defaultValues)\n  const [placing, setPlacing] = React.useState(false)\n  const [placed, setPlaced] = React.useState(false)\n  // Hand focus to the newly opened step so keyboard and screen reader users\n  // are not dropped on <body> when the Continue button unmounts.\n  const headings = React.useRef<(HTMLHeadingElement | null)[]>([])\n  const stepMounted = React.useRef(false)\n  React.useEffect(() => {\n    if (!stepMounted.current) {\n      stepMounted.current = true\n      return\n    }\n    const h = headings.current[step]\n    if (!h) return\n    h.focus({ preventScroll: true })\n    h.scrollIntoView({ block: \"nearest\", behavior: reduce ? \"auto\" : \"smooth\" })\n  }, [step, reduce])\n  const set = (k: keyof Form) => (e: React.ChangeEvent<HTMLInputElement>) => setForm((f) => ({ ...f, [k]: e.target.value }))\n\n  const method = METHODS.find((m) => m.id === form.method) ?? METHODS[0]\n  const subtotal = items.reduce((n, l) => n + l.price * l.quantity, 0)\n  const tax = Math.round(subtotal * taxRate)\n  const total = subtotal + method.price + tax\n\n  const summaries = [\n    form.email,\n    `${form.name}, ${form.address}, ${form.city} ${form.zip}`,\n    `${method.label}, ${method.price ? money(method.price) : \"Free\"}`,\n    `Card ending ${form.card.replace(/\\s/g, \"\").slice(-4)}`,\n  ]\n\n  const place = () => {\n    sk.emit(\"convert\", \"place_order\", total)\n    setPlacing(true)\n    window.setTimeout(() => {\n      setPlacing(false)\n      setPlaced(true)\n      setStep(4)\n      onPlaceOrder?.(form)\n    }, 1000)\n  }\n\n  const body = (i: number) => {\n    switch (i) {\n      case 0:\n        return <Input label=\"Email\" type=\"email\" autoComplete=\"email\" value={form.email} onChange={set(\"email\")} hint=\"Order updates go here.\" />\n      case 1:\n        return (\n          <div className=\"grid gap-4 sm:grid-cols-2\">\n            <div className=\"sm:col-span-2\"><Input label=\"Full name\" autoComplete=\"name\" value={form.name} onChange={set(\"name\")} /></div>\n            <div className=\"sm:col-span-2\"><Input label=\"Address\" autoComplete=\"street-address\" value={form.address} onChange={set(\"address\")} /></div>\n            <Input label=\"City, state\" autoComplete=\"address-level2\" value={form.city} onChange={set(\"city\")} />\n            <Input label=\"ZIP\" inputMode=\"numeric\" autoComplete=\"postal-code\" value={form.zip} onChange={set(\"zip\")} />\n          </div>\n        )\n      case 2:\n        return (\n          <fieldset className=\"grid gap-2\">\n            <legend className=\"sr-only\">Shipping method</legend>\n            {METHODS.map((m) => (\n              <label\n                key={m.id}\n                className={cn(\n                  \"flex min-h-14 cursor-pointer items-center gap-3 rounded-sk-card border px-4 transition-colors duration-200\",\n                  \"has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-sk-accent\",\n                  form.method === m.id ? \"border-sk-accent bg-sk-accent/[0.04]\" : \"border-sk-line hover:bg-sk-ink/[0.02]\"\n                )}\n              >\n                <input type=\"radio\" name=\"sk-checkout-method\" className=\"sr-only\" checked={form.method === m.id} onChange={() => setForm((f) => ({ ...f, method: m.id }))} />\n                <span aria-hidden className={cn(\"grid h-4 w-4 place-items-center rounded-full border\", form.method === m.id ? \"border-sk-accent\" : \"border-sk-muted/50\")}>\n                  {form.method === m.id ? <span className=\"h-2 w-2 rounded-full bg-sk-accent\" /> : null}\n                </span>\n                <span className=\"flex-1 text-sm\">\n                  <span className=\"font-medium\">{m.label}</span> <span className=\"text-sk-muted\">{m.note}</span>\n                </span>\n                <span className={cn(\"text-sm tabular-nums\", m.price ? \"text-sk-price\" : \"font-medium text-sk-savings\")}>{m.price ? money(m.price) : \"Free\"}</span>\n              </label>\n            ))}\n          </fieldset>\n        )\n      default:\n        return (\n          <div className=\"grid grid-cols-2 gap-4\">\n            <div className=\"col-span-2\"><Input label=\"Card number\" inputMode=\"numeric\" autoComplete=\"cc-number\" value={form.card} onChange={set(\"card\")} /></div>\n            <Input label=\"Expiry\" inputMode=\"numeric\" autoComplete=\"cc-exp\" value={form.exp} onChange={set(\"exp\")} />\n            <Input label=\"CVC\" inputMode=\"numeric\" autoComplete=\"cc-csc\" value={form.cvc} onChange={set(\"cvc\")} />\n          </div>\n        )\n    }\n  }\n\n  const ease: [number, number, number, number] = [0.16, 1, 0.3, 1]\n\n  return (\n    <div ref={sk.ref as React.Ref<HTMLDivElement>} {...sk.attrs} className={cn(\"mx-auto grid w-full max-w-6xl grid-cols-1 gap-8 px-4 py-10 sm:px-8 lg:grid-cols-[minmax(0,1fr)_380px] lg:gap-12\", className)}>\n      <div className=\"min-w-0\">\n        <p className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">{brandName}</p>\n        <h2 className=\"mt-2 font-sk-display text-2xl font-semibold leading-[1.1] tracking-[-0.02em]\">Checkout</h2>\n\n        <AnimatePresence initial={false}>\n          {placed ? (\n            <motion.div\n              initial={{ opacity: 0, y: reduce ? 0 : 8 }}\n              animate={{ opacity: 1, y: 0 }}\n              transition={{ duration: 0.4, ease }}\n              className=\"mt-6 flex items-center gap-3 rounded-sk-card border border-sk-savings/30 bg-sk-savings/10 px-4 py-3 text-sm text-sk-ink\"\n              role=\"status\"\n            >\n              <Check size={16} className=\"text-sk-savings\" />\n              Order placed. A confirmation is on its way to {form.email}.\n            </motion.div>\n          ) : null}\n        </AnimatePresence>\n\n        <ol className=\"mt-6 grid grid-cols-1 gap-3\">\n          {STEPS.map((label, i) => {\n            const current = i === step && !placed\n            const complete = i < step || placed\n            return (\n              <li key={label} className={cn(\"rounded-sk-card border bg-sk-card transition-colors duration-200\", current ? \"border-sk-ink/25\" : \"border-sk-line\")}>\n                <div className=\"flex min-h-16 items-center gap-4 px-5 py-3\">\n                  <span\n                    className={cn(\n                      \"grid h-7 w-7 shrink-0 place-items-center rounded-full font-sk-mono text-xs tabular-nums transition-colors duration-200\",\n                      complete ? \"bg-sk-savings/15 text-sk-savings\" : current ? \"bg-sk-accent text-sk-accent-ink\" : \"border border-sk-line text-sk-muted\"\n                    )}\n                  >\n                    {complete ? <Check size={14} strokeWidth={2.5} /> : String(i + 1).padStart(2, \"0\")}\n                  </span>\n                  <div className=\"min-w-0 flex-1\">\n                    <h3 ref={(el) => { headings.current[i] = el }} tabIndex={-1} className={cn(\"text-[15px] font-medium outline-none\", !current && !complete && \"text-sk-muted\")}>{label}</h3>\n                    {complete ? <p className=\"truncate text-sm text-sk-muted\">{summaries[i]}</p> : null}\n                  </div>\n                  {complete && !placed ? (\n                    <button\n                      type=\"button\"\n                      onClick={() => setStep(i)}\n                      aria-label={`Edit ${label.toLowerCase()}`}\n                      className=\"-mr-2 min-h-11 rounded-sk-btn px-3 text-sm font-medium text-sk-accent underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sk-accent\"\n                    >\n                      Edit\n                    </button>\n                  ) : null}\n                </div>\n                <AnimatePresence initial={false}>\n                  {current ? (\n                    <motion.div\n                      key=\"body\"\n                      initial={{ height: 0, opacity: 0 }}\n                      animate={{ height: \"auto\", opacity: 1 }}\n                      exit={{ height: 0, opacity: 0 }}\n                      transition={{ duration: reduce ? 0 : 0.36, ease }}\n                      className=\"overflow-hidden\"\n                    >\n                      <div className=\"grid grid-cols-1 gap-5 px-5 pb-5 pt-1\">\n                        {body(i)}\n                        {i < STEPS.length - 1 ? (\n                          <Button className=\"h-auto min-h-11 w-full whitespace-normal py-2.5 text-center leading-tight sm:w-auto sm:justify-self-start\" onClick={() => setStep(i + 1)}>\n                            Continue to {STEPS[i + 1].toLowerCase()}\n                          </Button>\n                        ) : (\n                          <Button size=\"lg\" className=\"w-full\" loading={placing} onClick={place}>\n                            <Lock size={14} />\n                            Pay <span className=\"tabular-nums\">{money(total)}</span>\n                          </Button>\n                        )}\n                      </div>\n                    </motion.div>\n                  ) : null}\n                </AnimatePresence>\n              </li>\n            )\n          })}\n        </ol>\n      </div>\n\n      <aside className=\"lg:sticky lg:top-6 lg:self-start\">\n        <div className=\"rounded-sk-card border border-sk-line bg-sk-surface/60 p-5 sm:p-6\">\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-14 w-14 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                    {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 tabular-nums text-sk-price\">{money(l.price * l.quantity)}</span>\n              </li>\n            ))}\n          </ul>\n          <dl className=\"mt-5 grid gap-2 border-t border-sk-line pt-4 text-sm\">\n            <div className=\"flex justify-between\"><dt className=\"text-sk-muted\">Subtotal</dt><dd className=\"tabular-nums\">{money(subtotal)}</dd></div>\n            <div className=\"flex justify-between\">\n              <dt className=\"text-sk-muted\">Shipping</dt>\n              <dd className={cn(\"tabular-nums\", !method.price && \"font-medium text-sk-savings\")}>{method.price ? money(method.price) : \"Free\"}</dd>\n            </div>\n            <div className=\"flex justify-between\"><dt className=\"text-sk-muted\">Estimated tax</dt><dd className=\"tabular-nums\">{money(tax)}</dd></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]\">{money(total)}</dd>\n            </div>\n          </dl>\n        </div>\n      </aside>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "components/storekit/checkout/checkout-layout.tsx"
    }
  ],
  "meta": {
    "section": "checkout",
    "variant": "single-page",
    "niche": [
      "any"
    ],
    "commerce": [
      "checkout",
      "shipping-method",
      "payment",
      "order-summary"
    ],
    "previewHeight": 720
  }
}