{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "checkout-form",
  "title": "Checkout Form",
  "description": "A one-page checkout — contact, delivery address, payment method and an order summary whose delivery, tax and payment fee are derived from the form in the same render, in integer minor units, so the lines always add up to the button and the money can never lag the address.",
  "dependencies": [
    "zod",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.zyeon.ai/r/credit-card-form.json"
  ],
  "files": [
    {
      "path": "src/registry/blocks/checkout-form.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDown, CircleAlert, CircleCheck, CreditCard, FileText, LoaderCircle, Wallet } from \"lucide-react\"\nimport { z } from \"zod\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  type CardBrandId,\n  detectCardBrand,\n  isLuhnValid,\n} from \"@/components/ui/credit-card-form\"\n\n/* -------------------------------------------------------------------------- */\n/* Money                                                                      */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Every amount in this block is an **integer in the currency's minor unit** — cents for USD,\n * yen for JPY, fils for KWD. Two reasons, and both of them are the difference between a\n * checkout and an incident:\n *\n * 1. `subtotal + shipping + fee + tax − discount === total` has to hold *exactly*. In floats it\n *    does not: `19.99 * 3` is `59.96999999999999`, and a summary that adds up to a cent less\n *    than the button says is a chargeback.\n * 2. The number of decimals is a property of the currency, not of the layout. JPY has none, KWD\n *    has three. `toFixed(2)` invents `¥1,234.00`, which is wrong in a way nobody notices until a\n *    Japanese customer sees it.\n *\n * So: integers everywhere, and `Intl.NumberFormat` decides how many digits to print.\n */\nexport type MinorUnits = number\n\n/** Non-finite input would poison every downstream sum and print \"$NaN\". Zero is the safe floor. */\nconst safeMinor = (value: number): MinorUnits => (Number.isFinite(value) ? Math.round(value) : 0)\n\n/** Quantities are whole units and never negative — a negative one would flip a line's sign. */\nconst safeQuantity = (value: number) => (Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0)\n\n/**\n * A formatter for `currency`, plus the divisor that turns minor units into what it wants.\n * The divisor is asked of `Intl` (`maximumFractionDigits` resolves to 2 for USD, 0 for JPY,\n * 3 for KWD) rather than hard-coded to 100 — that hard-coded 100 is the classic JPY bug.\n *\n * A currency code or locale tag `Intl` rejects throws a `RangeError`; caught here, because a\n * typo in a data feed must not blank the whole checkout. The fallback prints the code itself,\n * so the number is never shown without saying what it is.\n */\nfunction buildMoneyFormatter(locale: string, currency: string): (amount: MinorUnits) => string {\n  let format: Intl.NumberFormat\n  let carriesSymbol = true\n  try {\n    format = new Intl.NumberFormat(locale, { currency, style: \"currency\" })\n  } catch {\n    carriesSymbol = false\n    try {\n      format = new Intl.NumberFormat(locale, { maximumFractionDigits: 2, minimumFractionDigits: 2 })\n    } catch {\n      format = new Intl.NumberFormat(\"en-US\", { maximumFractionDigits: 2, minimumFractionDigits: 2 })\n    }\n  }\n  const divisor = 10 ** (format.resolvedOptions().maximumFractionDigits ?? 2)\n  return (amount: MinorUnits) => {\n    // `amount === 0 ? 0 : amount` normalises -0, so a zero line prints \"$0.00\", never \"-$0.00\".\n    const text = format.format((amount === 0 ? 0 : amount) / divisor)\n    return carriesSymbol ? text : `${currency} ${text}`\n  }\n}\n\n/** Basis points of a minor-unit base, rounded half-up — the rounding every tax authority uses. */\nconst applyBps = (base: MinorUnits, bps: number): MinorUnits =>\n  Number.isFinite(bps) ? Math.round((base * Math.max(0, bps)) / 10000) : 0\n\n/* -------------------------------------------------------------------------- */\n/* Cart                                                                       */\n/* -------------------------------------------------------------------------- */\n\nexport interface CheckoutLineItem {\n  id: string\n  name: string\n  /** Second line under the name: \"Size M · Ocean blue\". */\n  variant?: string\n  /** Whole units. Fractions and negatives are clamped away before anything is multiplied. */\n  quantity: number\n  /** Price of one unit, in minor units of the cart currency. */\n  unitAmount: MinorUnits\n}\n\nexport interface CheckoutDiscount {\n  /** Shown next to the label: \"SPRING10\". */\n  code?: string\n  label: string\n  /**\n   * A **positive** amount that gets subtracted. It is clamped to the subtotal, so the discount\n   * the summary prints is always the discount the total actually removed — a promo worth more\n   * than the basket cannot make the two disagree.\n   */\n  amount: MinorUnits\n}\n\nexport interface CheckoutCart {\n  /** ISO 4217. Decides the decimal places of every amount below — see `MinorUnits`. */\n  currency: string\n  items: readonly CheckoutLineItem[]\n  discount?: CheckoutDiscount\n}\n\n/* -------------------------------------------------------------------------- */\n/* Countries: address shape, tax and shipping in one row                      */\n/* -------------------------------------------------------------------------- */\n\nexport type CheckoutAddressField = \"line1\" | \"line2\" | \"city\" | \"region\" | \"postalCode\"\n\nexport interface CheckoutRegionRule {\n  /** The country's own word: State / Province / Prefecture. */\n  label: string\n  /** Omit for a free-text field. */\n  options?: readonly { value: string; label: string }[]\n  required: boolean\n}\n\nexport interface CheckoutPostalRule {\n  label: string\n  /** Placeholder, and quoted in the \"enter a valid …\" message. */\n  example: string\n  /** Tested against the trimmed, upper-cased input, so it must tolerate optional spacing. */\n  pattern: RegExp\n  inputMode?: \"text\" | \"numeric\"\n  maxLength: number\n}\n\nexport interface CheckoutTaxRule {\n  /** Row label: \"Sales tax\", \"VAT\", \"GST\". */\n  label: string\n  /** Basis points: 875 = 8.75%. */\n  rateBps: number\n  /** Overrides `rateBps` for a region value — this is what makes the state picker move the total. */\n  regionBps?: Readonly<Record<string, number>>\n  /**\n   * `\"all\"` (VAT/GST countries) taxes goods **and** delivery and fees; `\"goods\"` (US, CA) taxes\n   * the merchandise only. Getting this backwards is a few cents per order that never reconcile.\n   */\n  taxable: \"goods\" | \"all\"\n}\n\nexport interface CheckoutShippingRule {\n  /** Row label: \"Standard delivery\". */\n  label: string\n  /** Flat rate, in minor units of the **cart currency** — see the note on `CHECKOUT_COUNTRIES`. */\n  flat: MinorUnits\n  /** Free delivery once the discounted merchandise total reaches this. Omit for never. */\n  freeOver?: MinorUnits\n  /**\n   * Carrier surcharge for hard-to-reach postcodes, matched against the trimmed, upper-cased,\n   * space-stripped code. It survives the free-delivery threshold, because the carrier still bills\n   * it — which is exactly why the postcode, not just the country, has to move the summary.\n   */\n  remote?: { label: string; pattern: RegExp; amount: MinorUnits }\n}\n\nexport interface CheckoutCountry {\n  /** ISO 3166-1 alpha-2. */\n  code: string\n  name: string\n  /** The address rows this country writes, in the order it writes them. */\n  order: readonly CheckoutAddressField[]\n  line1Label: string\n  line2Label: string\n  cityLabel: string\n  /** Absent = this country has no administrative area (GB, DE). The row is not rendered. */\n  region?: CheckoutRegionRule\n  postalCode: CheckoutPostalRule\n  tax: CheckoutTaxRule\n  shipping: CheckoutShippingRule\n}\n\n/** `\"CA:California,NY:New York\"` → options. An entry with no colon uses its text as its value. */\nfunction regionOptions(spec: string): { value: string; label: string }[] {\n  return spec.split(\",\").map(entry => {\n    const at = entry.indexOf(\":\")\n    return at === -1\n      ? { value: entry, label: entry }\n      : { value: entry.slice(0, at), label: entry.slice(at + 1) }\n  })\n}\n\n/**\n * Six countries, chosen so the rules that actually differ are all represented: a state picker\n * with per-state tax (US), a province picker with per-province tax (CA), no administrative area\n * at all (GB, DE), the postcode written before the city (DE), and a big-endian address whose code\n * comes first (JP).\n *\n * **The money in here is illustrative.** Rates are national/state averages, not rooftop-accurate\n * jurisdictions, and the shipping figures are written in 2-decimal minor units (cents). Trim the\n * list to where you ship, replace the numbers with your carrier's, and hand real tax to a tax\n * engine through the `quote` prop — a checkout that guesses tax is a checkout that under-collects.\n * A cart in a 0-decimal currency (JPY) needs its own table: 800 means ¥800 there, not ¥8.00.\n */\nexport const CHECKOUT_COUNTRIES: readonly CheckoutCountry[] = [\n  {\n    code: \"US\",\n    name: \"United States\",\n    order: [\"line1\", \"line2\", \"city\", \"region\", \"postalCode\"],\n    line1Label: \"Street address\",\n    line2Label: \"Apartment, suite, unit (optional)\",\n    cityLabel: \"City\",\n    region: {\n      label: \"State\",\n      required: true,\n      options: regionOptions(\n        \"AL:Alabama,AK:Alaska,AZ:Arizona,AR:Arkansas,CA:California,CO:Colorado,CT:Connecticut,DE:Delaware,DC:District of Columbia,FL:Florida,GA:Georgia,HI:Hawaii,ID:Idaho,IL:Illinois,IN:Indiana,IA:Iowa,KS:Kansas,KY:Kentucky,LA:Louisiana,ME:Maine,MD:Maryland,MA:Massachusetts,MI:Michigan,MN:Minnesota,MS:Mississippi,MO:Missouri,MT:Montana,NE:Nebraska,NV:Nevada,NH:New Hampshire,NJ:New Jersey,NM:New Mexico,NY:New York,NC:North Carolina,ND:North Dakota,OH:Ohio,OK:Oklahoma,OR:Oregon,PA:Pennsylvania,RI:Rhode Island,SC:South Carolina,SD:South Dakota,TN:Tennessee,TX:Texas,UT:Utah,VT:Vermont,VA:Virginia,WA:Washington,WV:West Virginia,WI:Wisconsin,WY:Wyoming\",\n      ),\n    },\n    postalCode: {\n      label: \"ZIP code\",\n      example: \"94107\",\n      pattern: /^\\d{5}(-?\\d{4})?$/,\n      inputMode: \"numeric\",\n      maxLength: 10,\n    },\n    tax: {\n      label: \"Sales tax\",\n      rateBps: 600,\n      // Five states levy none; the rest are combined state + average local rates.\n      regionBps: {\n        AK: 0,\n        CA: 875,\n        DE: 0,\n        FL: 700,\n        IL: 875,\n        MT: 0,\n        NH: 0,\n        NY: 800,\n        OR: 0,\n        TX: 825,\n        WA: 1025,\n      },\n      taxable: \"goods\",\n    },\n    shipping: {\n      label: \"Standard delivery\",\n      flat: 795,\n      freeOver: 7500,\n      // Alaska, Hawaii and the Pacific territories: every carrier bills these separately.\n      remote: { label: \"Remote area surcharge\", pattern: /^(99|96[789])/, amount: 1400 },\n    },\n  },\n  {\n    code: \"CA\",\n    name: \"Canada\",\n    order: [\"line1\", \"line2\", \"city\", \"region\", \"postalCode\"],\n    line1Label: \"Street address\",\n    line2Label: \"Apartment, suite, unit (optional)\",\n    cityLabel: \"City\",\n    region: {\n      label: \"Province\",\n      required: true,\n      options: regionOptions(\n        \"AB:Alberta,BC:British Columbia,MB:Manitoba,NB:New Brunswick,NL:Newfoundland and Labrador,NS:Nova Scotia,NT:Northwest Territories,NU:Nunavut,ON:Ontario,PE:Prince Edward Island,QC:Quebec,SK:Saskatchewan,YT:Yukon\",\n      ),\n    },\n    postalCode: {\n      label: \"Postal code\",\n      example: \"K1A 0B1\",\n      pattern: /^[ABCEGHJ-NPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z] ?\\d[ABCEGHJ-NPRSTV-Z]\\d$/,\n      maxLength: 7,\n    },\n    tax: {\n      label: \"GST / HST\",\n      rateBps: 500,\n      regionBps: { BC: 1200, MB: 1200, NB: 1500, NL: 1500, NS: 1400, ON: 1300, PE: 1500, QC: 1498, SK: 1100 },\n      taxable: \"goods\",\n    },\n    shipping: {\n      label: \"Standard delivery\",\n      flat: 1295,\n      freeOver: 10000,\n      // The three territories: postal codes there start X or Y.\n      remote: { label: \"Northern surcharge\", pattern: /^[XY]/, amount: 2500 },\n    },\n  },\n  {\n    code: \"GB\",\n    name: \"United Kingdom\",\n    // No administrative area: a UK address is street → town → postcode, and the postcode is\n    // precise enough that asking for a county would be asking for something nobody writes.\n    order: [\"line1\", \"line2\", \"city\", \"postalCode\"],\n    line1Label: \"Address line 1\",\n    line2Label: \"Address line 2 (optional)\",\n    cityLabel: \"Town / City\",\n    postalCode: {\n      label: \"Postcode\",\n      example: \"SW1A 1AA\",\n      pattern: /^[A-Z]{1,2}\\d[A-Z\\d]? ?\\d[A-Z]{2}$/,\n      maxLength: 8,\n    },\n    tax: { label: \"VAT\", rateBps: 2000, taxable: \"all\" },\n    shipping: {\n      label: \"Standard delivery\",\n      flat: 599,\n      freeOver: 5000,\n      remote: { label: \"Highlands & Islands surcharge\", pattern: /^(IV|KW|HS|ZE|AB3[0-9])/, amount: 1200 },\n    },\n  },\n  {\n    code: \"DE\",\n    name: \"Germany\",\n    // \"10115 Berlin\" — the code is written before the city, so it is entered before it too.\n    order: [\"line1\", \"line2\", \"postalCode\", \"city\"],\n    line1Label: \"Street and house number\",\n    line2Label: \"Address supplement (optional)\",\n    cityLabel: \"City\",\n    postalCode: {\n      label: \"Postcode (PLZ)\",\n      example: \"10115\",\n      pattern: /^\\d{5}$/,\n      inputMode: \"numeric\",\n      maxLength: 5,\n    },\n    tax: { label: \"MwSt.\", rateBps: 1900, taxable: \"all\" },\n    shipping: { label: \"Standard delivery\", flat: 495, freeOver: 5000 },\n  },\n  {\n    code: \"JP\",\n    name: \"Japan\",\n    // Japanese addresses run large → small, and the 〒 code is written first.\n    order: [\"postalCode\", \"region\", \"city\", \"line1\", \"line2\"],\n    line1Label: \"Address (chome, banchi, go)\",\n    line2Label: \"Building, room number (optional)\",\n    cityLabel: \"City / Ward / Town\",\n    region: {\n      label: \"Prefecture\",\n      required: true,\n      options: regionOptions(\n        \"Hokkaido,Aomori,Iwate,Miyagi,Akita,Yamagata,Fukushima,Ibaraki,Tochigi,Gunma,Saitama,Chiba,Tokyo,Kanagawa,Niigata,Toyama,Ishikawa,Fukui,Yamanashi,Nagano,Gifu,Shizuoka,Aichi,Mie,Shiga,Kyoto,Osaka,Hyogo,Nara,Wakayama,Tottori,Shimane,Okayama,Hiroshima,Yamaguchi,Tokushima,Kagawa,Ehime,Kochi,Fukuoka,Saga,Nagasaki,Kumamoto,Oita,Miyazaki,Kagoshima,Okinawa\",\n      ),\n    },\n    postalCode: {\n      label: \"Postal code\",\n      example: \"150-0002\",\n      pattern: /^\\d{3}-?\\d{4}$/,\n      inputMode: \"numeric\",\n      maxLength: 8,\n    },\n    tax: { label: \"Consumption tax\", rateBps: 1000, taxable: \"all\" },\n    shipping: {\n      label: \"Standard delivery\",\n      flat: 800,\n      freeOver: 10000,\n      // Okinawa and the outer islands: 900–909.\n      remote: { label: \"Island surcharge\", pattern: /^90/, amount: 1200 },\n    },\n  },\n  {\n    code: \"AU\",\n    name: \"Australia\",\n    order: [\"line1\", \"line2\", \"city\", \"region\", \"postalCode\"],\n    line1Label: \"Street address\",\n    line2Label: \"Unit, level (optional)\",\n    cityLabel: \"Suburb\",\n    region: {\n      label: \"State / Territory\",\n      required: true,\n      options: regionOptions(\n        \"ACT:Australian Capital Territory,NSW:New South Wales,NT:Northern Territory,QLD:Queensland,SA:South Australia,TAS:Tasmania,VIC:Victoria,WA:Western Australia\",\n      ),\n    },\n    postalCode: {\n      label: \"Postcode\",\n      example: \"3000\",\n      pattern: /^\\d{4}$/,\n      inputMode: \"numeric\",\n      maxLength: 4,\n    },\n    tax: { label: \"GST\", rateBps: 1000, taxable: \"all\" },\n    shipping: {\n      label: \"Standard delivery\",\n      flat: 1195,\n      freeOver: 12000,\n      // NT and remote WA.\n      remote: { label: \"Remote area surcharge\", pattern: /^0[89]/, amount: 1800 },\n    },\n  },\n]\n\n/**\n * Shape used for a code the table does not list — a value restored from a database after someone\n * trimmed the list, or one set programmatically. Deliberately permissive so an unknown\n * destination degrades into a usable form instead of an unsubmittable one, and deliberately\n * charges nothing: guessing a foreign tax rate is worse than saying it is confirmed later.\n */\nexport const GENERIC_CHECKOUT_COUNTRY: Omit<CheckoutCountry, \"code\" | \"name\"> = {\n  order: [\"line1\", \"line2\", \"city\", \"region\", \"postalCode\"],\n  line1Label: \"Address line 1\",\n  line2Label: \"Address line 2 (optional)\",\n  cityLabel: \"City\",\n  region: { label: \"State / Province / Region\", required: false },\n  postalCode: {\n    label: \"Postal code\",\n    example: \"10115\",\n    pattern: /^[A-Z0-9][A-Z0-9 -]{1,11}$/,\n    maxLength: 12,\n  },\n  tax: { label: \"Tax\", rateBps: 0, taxable: \"goods\" },\n  shipping: { label: \"Delivery\", flat: 0 },\n}\n\n/** The rule for `code`, or a generic one carrying that code so the form still works. */\nexport function resolveCheckoutCountry(\n  code: string,\n  countries: readonly CheckoutCountry[] = CHECKOUT_COUNTRIES,\n): CheckoutCountry {\n  const found = countries.find(country => country.code === code)\n  if (found !== undefined) return found\n  return { ...GENERIC_CHECKOUT_COUNTRY, code, name: code === \"\" ? \"Elsewhere\" : code }\n}\n\n/** Upper-cased, separators stripped — the shape both postal patterns and surcharges compare against. */\nconst compactPostal = (raw: string) => raw.replace(/[\\s-]/g, \"\").toUpperCase()\n\n/* -------------------------------------------------------------------------- */\n/* Payment methods                                                            */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The three that need genuinely different fields — a card takes a PAN, a wallet takes nothing\n * because the wallet sheet collects it, and an invoice takes who to bill. Anything that only\n * changes a logo is a variant of one of these, not a fourth id.\n */\nexport type CheckoutPaymentMethodId = \"card\" | \"wallet\" | \"invoice\"\n\nexport interface CheckoutPaymentMethod {\n  id: CheckoutPaymentMethodId\n  label: string\n  /** One line under the label, in the picker. */\n  description: string\n  /** Basis points of the discounted merchandise total. 250 = 2.5%. Omit or 0 for no fee. */\n  feeBps?: number\n  /** Row label for the fee in the summary. Required if `feeBps` is set. */\n  feeLabel?: string\n  /** Overrides the pay button's label — a wallet does not \"pay\" here, it hands off. */\n  submitLabel?: string\n}\n\nexport const CHECKOUT_PAYMENT_METHODS: readonly CheckoutPaymentMethod[] = [\n  {\n    id: \"card\",\n    label: \"Card\",\n    description: \"Visa, Mastercard, Amex and the rest.\",\n  },\n  {\n    id: \"wallet\",\n    label: \"Wallet\",\n    description: \"Confirm in your wallet sheet — nothing to type here.\",\n    submitLabel: \"Continue to wallet\",\n  },\n  {\n    id: \"invoice\",\n    label: \"Pay by invoice\",\n    description: \"Net 30 for registered businesses.\",\n    feeBps: 250,\n    feeLabel: \"Invoicing fee (2.5%)\",\n  },\n]\n\nconst METHOD_ICONS: Record<CheckoutPaymentMethodId, React.ComponentType<{ className?: string }>> = {\n  card: CreditCard,\n  invoice: FileText,\n  wallet: Wallet,\n}\n\n/* -------------------------------------------------------------------------- */\n/* Values, totals, payload                                                    */\n/* -------------------------------------------------------------------------- */\n\nexport interface CheckoutFormValues {\n  email: string\n  country: string\n  name: string\n  line1: string\n  line2: string\n  city: string\n  region: string\n  postalCode: string\n  method: CheckoutPaymentMethodId\n  /** PAN digits only — the field owns the grouping, so no spaces ever live in the value. */\n  cardNumber: string\n  /** MMYY digits (\"0930\"); rendered as MM/YY. */\n  cardExpiry: string\n  cardCvc: string\n  cardName: string\n  company: string\n  taxId: string\n}\n\nexport type CheckoutField = Exclude<keyof CheckoutFormValues, \"method\">\n\n/** What the built-in quote produces. `total` is deliberately absent — see `CheckoutTotals`. */\nexport interface CheckoutQuoteParts {\n  subtotal: MinorUnits\n  shipping: MinorUnits\n  /** Row label for delivery; the built-in quote appends the surcharge when one applies. */\n  shippingLabel: string\n  /** Payment-method fee. 0 for card and wallet. */\n  fee: MinorUnits\n  feeLabel?: string\n  tax: MinorUnits\n  taxLabel: string\n  /** Positive amount that gets subtracted, already clamped to the subtotal. */\n  discount: MinorUnits\n  discountLabel?: string\n}\n\n/**\n * What the summary renders. `total` is **computed here from the parts, always** — a quote can\n * never hand back a total that disagrees with the lines above it, because it does not get to\n * supply one. That makes `subtotal + shipping + fee + tax − discount === total` structural\n * rather than something to remember to assert.\n */\nexport interface CheckoutTotals extends CheckoutQuoteParts {\n  currency: string\n  total: MinorUnits\n}\n\nexport interface CheckoutQuoteInput {\n  cart: CheckoutCart\n  /** The resolved rule for the country now selected — generic when the table does not list it. */\n  country: CheckoutCountry\n  countryCode: string\n  region: string\n  postalCode: string\n  method: CheckoutPaymentMethod\n  /** Sum of the line totals, already clamped. */\n  subtotal: MinorUnits\n  /** Clamped to the subtotal. */\n  discount: MinorUnits\n  /** Units in the basket — the sum of the clamped quantities, not the number of line entries. */\n  itemCount: number\n}\n\nexport type CheckoutQuoteFn = (input: CheckoutQuoteInput) => CheckoutQuoteParts\n\n/**\n * Tax and delivery from the country table. Pure and synchronous **on purpose**: the summary is\n * derived from the form state during render, so the new country and the new tax land in one\n * commit and there is no frame in which the address has moved and the money has not.\n *\n * Quoting against a real tax/rates service is asynchronous, and that is where the tear comes\n * back. The fix is not to await inside this function — it is to keep the answer keyed by the\n * address it was computed for, and render \"Calculated at the next step\" (never a stale number)\n * whenever the key does not match the address on screen. See the Prompt for the shape.\n */\nexport const defaultCheckoutQuote: CheckoutQuoteFn = input => {\n  const { country, discount, itemCount, method, postalCode, region, subtotal } = input\n  const net = Math.max(0, subtotal - discount)\n  const feeLabel = method.feeLabel\n  const fee = itemCount === 0 ? 0 : applyBps(net, method.feeBps ?? 0)\n\n  const { shipping: rule, tax } = country\n  const compact = compactPostal(postalCode)\n  const surcharge =\n    rule.remote !== undefined && compact !== \"\" && rule.remote.pattern.test(compact) ? rule.remote.amount : 0\n  const free = rule.freeOver !== undefined && net >= rule.freeOver\n  // The surcharge survives the free-delivery threshold: the carrier still bills it, so hiding it\n  // would mean eating it on every order to Alaska.\n  const shipping = itemCount === 0 ? 0 : (free ? 0 : rule.flat) + surcharge\n  const shippingLabel =\n    surcharge > 0 && rule.remote !== undefined ? `${rule.label} + ${rule.remote.label}` : rule.label\n\n  const rateBps = tax.regionBps?.[region] ?? tax.rateBps\n  const base = tax.taxable === \"all\" ? net + shipping + fee : net\n\n  return {\n    discount,\n    fee,\n    feeLabel,\n    shipping,\n    shippingLabel,\n    subtotal,\n    tax: applyBps(base, rateBps),\n    taxLabel: tax.label,\n  }\n}\n\nexport interface CheckoutAddress {\n  country: string\n  name: string\n  line1: string\n  line2?: string\n  city: string\n  /** Absent when the country has no administrative area. */\n  region?: string\n  postalCode: string\n}\n\nexport type CheckoutPaymentDetails =\n  | {\n      method: \"card\"\n      /**\n       * PAN digits. Never log this, never POST it to your own servers — see the security note on\n       * the component. It is here so a browser-side tokenizer can reach it, and nowhere else.\n       */\n      number: string\n      /** The only part of the number that is safe to store, show or send. */\n      last4: string\n      brand: CardBrandId\n      /** 1–12. */\n      expiryMonth: number\n      /** Four digits, expanded from the two typed ones. */\n      expiryYear: number\n      cvc: string\n      name: string\n    }\n  | { method: \"wallet\" }\n  | { method: \"invoice\"; company: string; taxId: string }\n\nexport interface CheckoutPayload {\n  email: string\n  shippingAddress: CheckoutAddress\n  payment: CheckoutPaymentDetails\n  /** The same numbers the summary printed, so the server can compare before it charges. */\n  totals: CheckoutTotals\n}\n\n/* -------------------------------------------------------------------------- */\n/* Validation (zod only)                                                      */\n/* -------------------------------------------------------------------------- */\n\n/** MMYY → a four-digit year in the reference century. 01/05 stays 2005, not 2105. */\nconst expandYear = (twoDigit: number, reference: Date) =>\n  Math.floor(reference.getFullYear() / 100) * 100 + twoDigit\n\n/**\n * The whole rule set, rebuilt whenever the country or the payment method changes — because both\n * of them change what \"valid\" means. The form carries `noValidate`, so the browser never runs a\n * competing rule set; `type=\"email\"` survives only for the phone keyboard (the native rule\n * accepts `a@b`, which every mail provider bounces).\n *\n * Exported so a server route can run the identical check on the payload it receives.\n */\nexport function buildCheckoutSchema(options: {\n  country: CheckoutCountry\n  method: CheckoutPaymentMethodId\n  /** Reference \"today\" for the expiry rule. Read at parse time, never during render. */\n  now?: Date\n}) {\n  const { country, method, now } = options\n  const line1Label = country.line1Label\n  const cityLabel = country.cityLabel\n\n  return z\n    .object({\n      email: z\n        .string()\n        .trim()\n        .min(1, \"Enter the address the receipt should go to.\")\n        .pipe(z.email(\"That doesn't look like an email address.\")),\n      country: z.string().trim().min(1, \"Choose where this order ships.\"),\n      name: z.string().trim().min(1, \"Tell the carrier who to deliver to.\"),\n      line1: z.string().trim().min(1, `${line1Label} is required.`),\n      line2: z.string(),\n      city: z.string().trim().min(1, `${cityLabel} is required.`),\n      region: z.string(),\n      postalCode: z.string(),\n      method: z.enum([\"card\", \"wallet\", \"invoice\"]),\n      cardNumber: z.string(),\n      cardExpiry: z.string(),\n      cardCvc: z.string(),\n      cardName: z.string(),\n      company: z.string(),\n      taxId: z.string(),\n    })\n    .superRefine((values, ctx) => {\n      // Every branch below guards its own inputs. Refinements all run — a failure in one does\n      // not stop the next — so a branch that assumed a well-formed value would throw out of\n      // `safeParse` instead of returning an error object, and the caller would get nothing.\n      const region = typeof values.region === \"string\" ? values.region.trim() : \"\"\n      const postal = typeof values.postalCode === \"string\" ? values.postalCode.trim() : \"\"\n\n      const regionRule = country.region\n      if (regionRule !== undefined && regionRule.required && region === \"\") {\n        ctx.addIssue({ code: \"custom\", message: `${regionRule.label} is required.`, path: [\"region\"] })\n      }\n\n      const postalRule = country.postalCode\n      if (postal === \"\") {\n        ctx.addIssue({ code: \"custom\", message: `${postalRule.label} is required.`, path: [\"postalCode\"] })\n      } else if (!postalRule.pattern.test(postal.toUpperCase())) {\n        ctx.addIssue({\n          code: \"custom\",\n          message: `Enter a valid ${postalRule.label} — for example ${postalRule.example}.`,\n          path: [\"postalCode\"],\n        })\n      }\n\n      if (method === \"card\") {\n        const digits = typeof values.cardNumber === \"string\" ? values.cardNumber : \"\"\n        const brand = detectCardBrand(digits)\n        if (digits === \"\") {\n          ctx.addIssue({ code: \"custom\", message: \"Card number is required.\", path: [\"cardNumber\"] })\n        } else if (!brand.lengths.includes(digits.length)) {\n          ctx.addIssue({ code: \"custom\", message: \"Card number is incomplete.\", path: [\"cardNumber\"] })\n        } else if (!isLuhnValid(digits)) {\n          ctx.addIssue({ code: \"custom\", message: \"Card number is invalid — check for a typo.\", path: [\"cardNumber\"] })\n        }\n\n        const expiry = typeof values.cardExpiry === \"string\" ? values.cardExpiry : \"\"\n        if (expiry === \"\") {\n          ctx.addIssue({ code: \"custom\", message: \"Expiry date is required.\", path: [\"cardExpiry\"] })\n        } else if (expiry.length < 4) {\n          ctx.addIssue({ code: \"custom\", message: \"Expiry date is incomplete.\", path: [\"cardExpiry\"] })\n        } else {\n          const month = Number(expiry.slice(0, 2))\n          if (!(month >= 1 && month <= 12)) {\n            ctx.addIssue({ code: \"custom\", message: \"Expiry month must be 01–12.\", path: [\"cardExpiry\"] })\n          } else {\n            // Read here, inside the parse, so render stays free of `new Date()`.\n            const reference = now ?? new Date()\n            const year = expandYear(Number(expiry.slice(2, 4)), reference)\n            const referenceYear = reference.getFullYear()\n            const referenceMonth = reference.getMonth() + 1\n            // A card is good through the end of its month, so only a strictly earlier one expired.\n            if (year < referenceYear || (year === referenceYear && month < referenceMonth)) {\n              ctx.addIssue({ code: \"custom\", message: \"This card has expired.\", path: [\"cardExpiry\"] })\n            }\n          }\n        }\n\n        const cvc = typeof values.cardCvc === \"string\" ? values.cardCvc : \"\"\n        if (cvc === \"\") {\n          ctx.addIssue({ code: \"custom\", message: `${brand.cvcLabel} is required.`, path: [\"cardCvc\"] })\n        } else if (!brand.cvcLengths.includes(cvc.length)) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: `${brand.cvcLabel} must be ${brand.cvcLengths.join(\" or \")} digits.`,\n            path: [\"cardCvc\"],\n          })\n        }\n\n        if (typeof values.cardName !== \"string\" || values.cardName.trim() === \"\") {\n          ctx.addIssue({ code: \"custom\", message: \"Enter the name printed on the card.\", path: [\"cardName\"] })\n        }\n      }\n\n      if (method === \"invoice\") {\n        if (typeof values.company !== \"string\" || values.company.trim() === \"\") {\n          ctx.addIssue({ code: \"custom\", message: \"Invoices need a company to bill.\", path: [\"company\"] })\n        }\n        const taxId = typeof values.taxId === \"string\" ? values.taxId.trim() : \"\"\n        if (taxId === \"\") {\n          ctx.addIssue({ code: \"custom\", message: \"VAT / Tax ID is required for invoicing.\", path: [\"taxId\"] })\n        } else if (taxId.replace(/[\\s-]/g, \"\").length < 6) {\n          ctx.addIssue({ code: \"custom\", message: \"That VAT / Tax ID looks too short.\", path: [\"taxId\"] })\n        }\n      }\n    })\n}\n\n/* -------------------------------------------------------------------------- */\n/* Digit fields: grouping and caret                                           */\n/* -------------------------------------------------------------------------- */\n\ntype DigitField = \"cardNumber\" | \"cardExpiry\" | \"cardCvc\"\n\nconst isDigitChar = (char: string | undefined) => char !== undefined && char >= \"0\" && char <= \"9\"\n\nconst countDigits = (text: string) => {\n  let count = 0\n  for (const char of text) if (isDigitChar(char)) count++\n  return count\n}\n\n/** Space-separated groups; whatever is left after the last group becomes one trailing group. */\nfunction groupDigits(digits: string, groups: readonly number[]): string {\n  const parts: string[] = []\n  let index = 0\n  for (const size of groups) {\n    if (index >= digits.length) break\n    parts.push(digits.slice(index, index + size))\n    index += size\n  }\n  if (index < digits.length) parts.push(digits.slice(index))\n  return parts.join(\" \")\n}\n\n/**\n * Display index just after the `n`-th digit. \"How many digits are behind the caret\" is the only\n * caret coordinate that survives regrouping — without it, editing the middle of a card number\n * flings the cursor to the end on every keystroke. (Same technique as `credit-card-form`, which\n * cannot be reused directly here because it renders its own `<form>` element.)\n */\nfunction caretAfterDigits(display: string, n: number): number {\n  let caret = 0\n  if (n > 0) {\n    caret = display.length\n    let seen = 0\n    for (let i = 0; i < display.length; i++) {\n      if (!isDigitChar(display[i])) continue\n      seen++\n      if (seen === n) {\n        caret = i + 1\n        break\n      }\n    }\n  }\n  // A caret parked in front of a trailing separator belongs after it, or the next keystroke reads\n  // as being typed behind the slash. Only steps over separators with no digit left after them.\n  while (caret < display.length && !isDigitChar(display[caret]) && countDigits(display.slice(caret)) === 0) caret++\n  return caret\n}\n\n/**\n * MMYY, normalised while it is typed: a lone 2–9 can only be a one-digit month, so it is padded\n * (\"5\" → \"05\") and the year always starts at index 2. Nothing is ever dropped — this re-reads the\n * whole digit stream on every edit, deletions included, so a rule that discarded a digit would\n * fire on digits the user never typed.\n */\nfunction normalizeExpiryDigits(input: string): string {\n  let out = \"\"\n  for (const char of input) {\n    if (!isDigitChar(char)) continue\n    if (out.length === 0) {\n      out = char > \"1\" ? `0${char}` : char\n      continue\n    }\n    if (out.length >= 4) break\n    out += char\n  }\n  return out\n}\n\nconst formatExpiry = (digits: string) => (digits.length < 2 ? digits : `${digits.slice(0, 2)}/${digits.slice(2, 4)}`)\n\n/* -------------------------------------------------------------------------- */\n/* Copy                                                                       */\n/* -------------------------------------------------------------------------- */\n\n/** Every user-visible string that is not a country rule or a zod message. The i18n surface. */\nexport const CHECKOUT_MESSAGES = {\n  contactHeading: \"Contact\",\n  contactNote: \"The receipt and delivery updates go to this address.\",\n  emailLabel: \"Email\",\n  emailPlaceholder: \"you@example.com\",\n  shippingHeading: \"Delivery address\",\n  countryLabel: \"Country / Region\",\n  nameLabel: \"Full name\",\n  namePlaceholder: \"Alex Morgan\",\n  selectPlaceholder: (label: string) => `Select ${label.toLowerCase()}`,\n  paymentHeading: \"Payment\",\n  cardNumberLabel: \"Card number\",\n  cardExpiryLabel: \"Expiry date\",\n  cardNameLabel: \"Name on card\",\n  companyLabel: \"Company name\",\n  taxIdLabel: \"VAT / Tax ID\",\n  walletNote:\n    \"Nothing to fill in here. Pressing the button opens your wallet sheet, and the order is placed once you confirm there.\",\n  invoiceNote: \"We email the invoice the moment the order ships. Payment is due 30 days after that.\",\n  summaryHeading: \"Order summary\",\n  emptyCart: \"There is nothing in this order yet.\",\n  emptyCartHint: \"Add something to the basket and the totals will appear here.\",\n  subtotalLabel: \"Subtotal\",\n  totalLabel: \"Total\",\n  freeBadge: \"Free\",\n  quantityLabel: (quantity: number) => `Qty ${quantity}`,\n  estimateNote: \"Delivery and tax are estimated from the address above and confirmed when the order is placed.\",\n  payLabel: \"Pay\",\n  pendingLabel: \"Processing…\",\n  successTitle: \"Payment authorised\",\n  successBody: \"Hang tight — we're taking you to your confirmation.\",\n  failureFallback: \"We couldn't take that payment. Nothing you typed was lost — check the details and try again.\",\n  emptyCartFailure: \"This order is empty, so there is nothing to pay for yet.\",\n  securityNote: \"Card details are used in your browser only.\",\n}\n\nexport type CheckoutMessages = typeof CHECKOUT_MESSAGES\n\n/**\n * Attach field errors to a rejection so the form can put them on the right rows and move focus\n * to the first of them. Duck-typed on read, so an error crossing a bundle boundary still works.\n */\nexport function checkoutSubmitError(\n  message: string,\n  fieldErrors?: Partial<Record<CheckoutField, string>>,\n): Error & { fieldErrors?: Partial<Record<CheckoutField, string>> } {\n  const error: Error & { fieldErrors?: Partial<Record<CheckoutField, string>> } = new Error(message)\n  if (fieldErrors !== undefined) error.fieldErrors = fieldErrors\n  return error\n}\n\nfunction fieldErrorsOf(error: unknown): Partial<Record<CheckoutField, string>> | undefined {\n  if (typeof error !== \"object\" || error === null) return undefined\n  const candidate = (error as { fieldErrors?: unknown }).fieldErrors\n  if (typeof candidate !== \"object\" || candidate === null) return undefined\n  const out: Partial<Record<CheckoutField, string>> = {}\n  for (const [key, message] of Object.entries(candidate as Record<string, unknown>)) {\n    if (typeof message === \"string\" && message !== \"\") out[key as CheckoutField] = message\n  }\n  return Object.keys(out).length === 0 ? undefined : out\n}\n\n/** A rejection that is not an `Error` still has to become a sentence a human can read. */\nfunction rejectionText(error: unknown, fallback: string): string {\n  if (error instanceof Error && error.message !== \"\") return error.message\n  if (typeof error === \"string\" && error !== \"\") return error\n  return fallback\n}\n\n/* -------------------------------------------------------------------------- */\n/* Styling primitives                                                         */\n/* -------------------------------------------------------------------------- */\n\nconst CONTROL_CLASS = cn(\n  \"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 text-sm text-foreground shadow-xs\",\n  \"outline-none transition-colors motion-reduce:transition-none placeholder:text-muted-foreground\",\n  \"focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50\",\n  \"disabled:cursor-not-allowed disabled:opacity-50\",\n  \"aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20\",\n)\n\n/**\n * A native `<select>`, on purpose: a listbox built out of divs cannot be filled by the browser's\n * address autofill and loses the platform picker on phones — and autofill is most of what a\n * delivery address form is for. The option rows carry explicit token colours because browsers\n * that paint the popup from the control's own colours would otherwise draw near-white text on a\n * white listbox in dark mode.\n */\nconst SELECT_CLASS = \"appearance-none bg-transparent pr-9 [&>option]:bg-background [&>option]:text-foreground\"\n\nfunction Field({\n  children,\n  className,\n  controlId,\n  error,\n  errorId,\n  hint,\n  hintId,\n  label,\n}: {\n  children: React.ReactNode\n  className?: string\n  controlId: string\n  error: string | undefined\n  errorId: string\n  hint?: string\n  hintId: string\n  label: string\n}) {\n  return (\n    <div className={cn(\"grid min-w-0 content-start gap-1.5\", className)}>\n      <label className=\"text-sm font-medium text-foreground\" htmlFor={controlId}>\n        {label}\n      </label>\n      {children}\n      {/* The message slot keeps its height whether or not it holds a message: an error appearing\n          on blur must not push the pay button out from under a cursor already on its way down, or\n          the first press on it is silently lost. The error replaces the hint, so aria-describedby\n          only ever points at a node that exists. */}\n      <div className=\"min-h-4 text-xs leading-4\">\n        {error !== undefined ? (\n          <p className=\"font-medium text-destructive\" id={errorId}>\n            {error}\n          </p>\n        ) : hint !== undefined ? (\n          <p className=\"text-muted-foreground\" id={hintId}>\n            {hint}\n          </p>\n        ) : null}\n      </div>\n    </div>\n  )\n}\n\nfunction SummaryRow({\n  emphasis = false,\n  badge,\n  className,\n  label,\n  slot,\n  value,\n}: {\n  emphasis?: boolean\n  badge?: string\n  className?: string\n  label: string\n  slot: string\n  value: string\n}) {\n  return (\n    <div className={cn(\"flex items-start justify-between gap-3\", className)} data-slot={`checkout-${slot}`}>\n      <dt className={cn(\"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1\", emphasis && \"font-medium text-foreground\")}>\n        <span className=\"[overflow-wrap:anywhere]\">{label}</span>\n        {badge !== undefined && (\n          <span className=\"rounded-full border px-1.5 py-0.5 text-[10px] leading-none font-medium tracking-wide text-muted-foreground uppercase\">\n            {badge}\n          </span>\n        )}\n      </dt>\n      <dd className={cn(\"shrink-0 tabular-nums\", emphasis ? \"font-semibold text-foreground\" : \"text-foreground\")}>\n        {value}\n      </dd>\n    </div>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Component                                                                  */\n/* -------------------------------------------------------------------------- */\n\nexport type CheckoutStatus = \"idle\" | \"submitting\" | \"error\" | \"success\"\n\nexport interface CheckoutFormProps\n  extends Omit<React.FormHTMLAttributes<HTMLFormElement>, \"onSubmit\" | \"defaultValue\" | \"children\"> {\n  /** What is being bought, and in which currency. Every amount is in that currency's minor unit. */\n  cart: CheckoutCart\n  /**\n   * Owns the payment call — the block never talks to a network itself. It receives the parsed\n   * values and the totals it printed. Resolve to swap the button for the authorised panel; reject\n   * to show a retryable failure with **every field still filled in**. Reject with\n   * `checkoutSubmitError(message, { cardNumber: \"…\" })` to put the reason on a specific row and\n   * move focus there.\n   */\n  onSubmit: (payload: CheckoutPayload) => void | Promise<void>\n  /** Seeds the form — a signed-in customer's address, or a restored session. */\n  defaultValues?: Partial<CheckoutFormValues>\n  /** Where you ship, with each destination's tax and delivery rules. */\n  countries?: readonly CheckoutCountry[]\n  /** Used when `defaultValues.country` is absent. Falls back to the first row of `countries`. */\n  defaultCountry?: string\n  /** Which ways to pay to offer, in the order to offer them. */\n  methods?: readonly CheckoutPaymentMethod[]\n  /** Replaces the built-in tax/delivery maths. Must be pure and synchronous — see `defaultCheckoutQuote`. */\n  quote?: CheckoutQuoteFn\n  /** Explicit on purpose: `Intl.NumberFormat(undefined)` formats differently on server and client. */\n  locale?: string\n  /** Reference \"today\" for the card expiry rule, so it is testable. Read at submit, never in render. */\n  now?: Date\n  /** Disables every control. */\n  disabled?: boolean\n  /** Overrides for any of the built-in strings; field errors come from the schema. */\n  messages?: Partial<CheckoutMessages>\n}\n\nconst CARD_FIELDS: readonly CheckoutField[] = [\"cardNumber\", \"cardExpiry\", \"cardCvc\", \"cardName\"]\nconst INVOICE_FIELDS: readonly CheckoutField[] = [\"company\", \"taxId\"]\n\n/**\n * Rows that fit two-per-line once the column is wide enough. Everything else takes the whole\n * line, which is why the wide rows carry `@[30rem]:col-span-2` where they are rendered.\n */\n\n/**\n * A one-page checkout: contact, delivery address, payment method and a live order summary.\n *\n * One page rather than steps, and that is the whole design. The summary has to react to the\n * address (tax and delivery) and to the payment method (an invoicing fee) — putting those on\n * different screens hides exactly the coupling that goes wrong. For a stepped flow, drive\n * `form-wizard` and mount the pieces inside it.\n *\n * SECURITY — this is UI. It formats, brands and validates a card, then hands you the digits in\n * `onSubmit`; it does not tokenize, encrypt or transmit anything. A real payment flow must never\n * let a raw PAN reach your servers, your logs or your analytics: mount a PCI-compliant hosted\n * field set (Stripe Elements, Adyen Web Components, Braintree Hosted Fields) and let the\n * processor's iframe own the card data. The card inputs carry no `name` attribute on purpose, so\n * a stray native submit cannot post a card number anywhere, and the number is never rendered\n * outside its own field — not in the summary, not in a live region, not in a `title`.\n */\nexport const CheckoutForm = React.forwardRef<HTMLFormElement, CheckoutFormProps>(function CheckoutForm(\n  {\n    cart,\n    onSubmit,\n    defaultValues,\n    countries = CHECKOUT_COUNTRIES,\n    defaultCountry,\n    methods = CHECKOUT_PAYMENT_METHODS,\n    quote = defaultCheckoutQuote,\n    locale = \"en-US\",\n    now,\n    disabled = false,\n    messages,\n    className,\n    ...rest\n  },\n  forwardedRef,\n) {\n  const copy = React.useMemo(() => ({ ...CHECKOUT_MESSAGES, ...messages }), [messages])\n  const uid = React.useId()\n\n  const methodList = methods.length > 0 ? methods : CHECKOUT_PAYMENT_METHODS\n  const initialCountry = defaultValues?.country ?? defaultCountry ?? countries[0]?.code ?? \"\"\n  const initialMethod = defaultValues?.method ?? methodList[0].id\n\n  const [raw, setRaw] = React.useState<CheckoutFormValues>(() => ({\n    email: defaultValues?.email ?? \"\",\n    country: initialCountry,\n    name: defaultValues?.name ?? \"\",\n    line1: defaultValues?.line1 ?? \"\",\n    line2: defaultValues?.line2 ?? \"\",\n    city: defaultValues?.city ?? \"\",\n    region: defaultValues?.region ?? \"\",\n    postalCode: defaultValues?.postalCode ?? \"\",\n    method: initialMethod,\n    cardNumber: (defaultValues?.cardNumber ?? \"\").replace(/\\D/g, \"\"),\n    cardExpiry: normalizeExpiryDigits(defaultValues?.cardExpiry ?? \"\"),\n    cardCvc: (defaultValues?.cardCvc ?? \"\").replace(/\\D/g, \"\"),\n    cardName: defaultValues?.cardName ?? \"\",\n    company: defaultValues?.company ?? \"\",\n    taxId: defaultValues?.taxId ?? \"\",\n  }))\n  const [errors, setErrors] = React.useState<Partial<Record<CheckoutField, string>>>({})\n  const [status, setStatus] = React.useState<CheckoutStatus>(\"idle\")\n  const [failure, setFailure] = React.useState<string | null>(null)\n\n  const country = React.useMemo(\n    () => resolveCheckoutCountry(raw.country, countries),\n    [raw.country, countries],\n  )\n  const method = methodList.find(entry => entry.id === raw.method) ?? methodList[0]\n\n  /**\n   * Values scoped to what is actually on screen. Computed on every render rather than cleared on\n   * every switch, which is what makes it impossible for a hidden row to smuggle a stale value\n   * into the payload: leave the US and the state is gone from it, leave the card and the number\n   * is gone from it.\n   *\n   * The raw entry survives in component state, so tapping \"wallet\" and back does not make anyone\n   * retype a card. It is memory only — never rendered, never submitted, never in the DOM — and it\n   * dies with the component. If your threat model says a PAN must not outlive the card panel,\n   * clear those keys in `setMethod` as well; the payload is unaffected either way.\n   */\n  const values = React.useMemo<CheckoutFormValues>(() => {\n    const next = { ...raw, method: method.id }\n    if (country.region === undefined) next.region = \"\"\n    if (method.id !== \"card\") {\n      next.cardNumber = \"\"\n      next.cardExpiry = \"\"\n      next.cardCvc = \"\"\n      next.cardName = \"\"\n    }\n    if (method.id !== \"invoice\") {\n      next.company = \"\"\n      next.taxId = \"\"\n    }\n    return next\n  }, [raw, country, method])\n\n  /* ---------------------------------------------------------------------- */\n  /* Money — derived during render, so the address and the total move together */\n  /* ---------------------------------------------------------------------- */\n\n  const money = React.useMemo(() => buildMoneyFormatter(locale, cart.currency), [locale, cart.currency])\n\n  const lines = React.useMemo(\n    () =>\n      cart.items.map(item => {\n        const quantity = safeQuantity(item.quantity)\n        const unitAmount = safeMinor(item.unitAmount)\n        return { item, quantity, unitAmount, lineTotal: unitAmount * quantity }\n      }),\n    [cart.items],\n  )\n\n  // Units, not line entries. A basket whose only line has fallen to quantity 0 is empty in every\n  // sense that matters — counting the rows instead would quote it delivery, tax that delivery in a\n  // VAT country, and hand the customer a bill for an order of nothing.\n  const itemCount = lines.reduce((sum, line) => sum + line.quantity, 0)\n\n  const totals = React.useMemo<CheckoutTotals>(() => {\n    const subtotal = lines.reduce((sum, line) => sum + line.lineTotal, 0)\n    // Clamped, so the discount printed is always the discount subtracted — a promo worth more\n    // than the basket cannot make the summary and the total disagree.\n    const discount = Math.min(Math.max(0, safeMinor(cart.discount?.amount ?? 0)), subtotal)\n    const parts = quote({\n      cart,\n      country,\n      countryCode: values.country,\n      discount,\n      itemCount,\n      method,\n      postalCode: values.postalCode,\n      region: values.region,\n      subtotal,\n    })\n    // A custom `quote` is consumer code; sanitising here means no NaN, no fraction of a cent and\n    // no negative delivery charge can reach the arithmetic below.\n    const shipping = Math.max(0, safeMinor(parts.shipping))\n    const fee = Math.max(0, safeMinor(parts.fee))\n    const tax = Math.max(0, safeMinor(parts.tax))\n    const appliedDiscount = Math.min(Math.max(0, safeMinor(parts.discount)), subtotal)\n    return {\n      currency: cart.currency,\n      discount: appliedDiscount,\n      discountLabel: parts.discountLabel ?? cart.discount?.label,\n      fee,\n      feeLabel: parts.feeLabel,\n      shipping,\n      shippingLabel: parts.shippingLabel,\n      subtotal,\n      tax,\n      taxLabel: parts.taxLabel,\n      // The one place a total is ever produced. Nothing else may supply one.\n      total: subtotal + shipping + fee + tax - appliedDiscount,\n    }\n  }, [lines, itemCount, cart, quote, country, method, values.country, values.postalCode, values.region])\n\n  const cartEmpty = itemCount === 0\n\n  /* ---------------------------------------------------------------------- */\n  /* Field plumbing                                                         */\n  /* ---------------------------------------------------------------------- */\n\n  const addressOrder = country.order\n  const fieldOrder = React.useMemo<CheckoutField[]>(() => {\n    const list: CheckoutField[] = [\"email\", \"country\", \"name\", ...addressOrder]\n    if (method.id === \"card\") list.push(...CARD_FIELDS)\n    if (method.id === \"invoice\") list.push(...INVOICE_FIELDS)\n    return list\n  }, [addressOrder, method.id])\n\n  const fieldId = (field: CheckoutField) => `${uid}-${field}`\n  const errorId = (field: CheckoutField) => `${uid}-${field}-error`\n  const hintId = (field: CheckoutField) => `${uid}-${field}-hint`\n  const failureId = `${uid}-failure`\n  const summaryHeadingId = `${uid}-summary`\n\n  const emailRef = React.useRef<HTMLInputElement>(null)\n  const countryRef = React.useRef<HTMLSelectElement>(null)\n  const nameRef = React.useRef<HTMLInputElement>(null)\n  const line1Ref = React.useRef<HTMLInputElement>(null)\n  const line2Ref = React.useRef<HTMLInputElement>(null)\n  const cityRef = React.useRef<HTMLInputElement>(null)\n  // The area is a select where the list is enumerated and an input where it is free text; only\n  // one of the two is ever mounted.\n  const regionSelectRef = React.useRef<HTMLSelectElement>(null)\n  const regionInputRef = React.useRef<HTMLInputElement>(null)\n  const postalCodeRef = React.useRef<HTMLInputElement>(null)\n  const cardNumberRef = React.useRef<HTMLInputElement>(null)\n  const cardExpiryRef = React.useRef<HTMLInputElement>(null)\n  const cardCvcRef = React.useRef<HTMLInputElement>(null)\n  const cardNameRef = React.useRef<HTMLInputElement>(null)\n  const companyRef = React.useRef<HTMLInputElement>(null)\n  const taxIdRef = React.useRef<HTMLInputElement>(null)\n  const submitRef = React.useRef<HTMLButtonElement>(null)\n  /** Caret to re-apply after React commits a controlled digit field (see the layout effect). */\n  const caretRef = React.useRef<{ el: HTMLInputElement; caret: number } | null>(null)\n\n  /**\n   * Flipped synchronously at the top of the handler. State cannot do this job: five presses\n   * inside one tick all read the same stale `status`, and the customer is charged five times.\n   */\n  const pendingRef = React.useRef(false)\n  // Set in the effect body as well as cleared in cleanup: StrictMode runs mount → cleanup → mount\n  // in dev, so a ref that is only ever cleared reads false for the live instance.\n  const mountedRef = React.useRef(false)\n  // Latest-ref: consumers pass an inline arrow, so this must never sit in a dependency array.\n  const onSubmitRef = React.useRef(onSubmit)\n  React.useEffect(() => {\n    onSubmitRef.current = onSubmit\n  })\n  React.useEffect(() => {\n    mountedRef.current = true\n    return () => {\n      mountedRef.current = false\n    }\n  }, [])\n\n  // Validation reads the value through a ref: a blur fired by focus moving out of a field lands\n  // before React commits the edit that caused it.\n  const valuesRef = React.useRef(values)\n  React.useEffect(() => {\n    valuesRef.current = values\n  }, [values])\n\n  const fieldNode = (field: CheckoutField): HTMLElement | null => {\n    switch (field) {\n      case \"email\":\n        return emailRef.current\n      case \"country\":\n        return countryRef.current\n      case \"name\":\n        return nameRef.current\n      case \"line1\":\n        return line1Ref.current\n      case \"line2\":\n        return line2Ref.current\n      case \"city\":\n        return cityRef.current\n      case \"region\":\n        return regionSelectRef.current ?? regionInputRef.current\n      case \"postalCode\":\n        return postalCodeRef.current\n      case \"cardNumber\":\n        return cardNumberRef.current\n      case \"cardExpiry\":\n        return cardExpiryRef.current\n      case \"cardCvc\":\n        return cardCvcRef.current\n      case \"cardName\":\n        return cardNameRef.current\n      case \"company\":\n        return companyRef.current\n      case \"taxId\":\n        return taxIdRef.current\n    }\n  }\n\n  /**\n   * Returns whether focus actually landed. Checking is not paranoia: a row can have been removed\n   * by a country or method switch, and a control that is still carrying `disabled` from the\n   * in-flight payment **refuses focus silently** — `focus()` neither throws nor moves anything,\n   * so a caller that assumed success would report \"focused the first bad field\" while the caret\n   * sat somewhere else entirely.\n   */\n  const focusField = (field: CheckoutField) => {\n    const node = fieldNode(field)\n    if (node === null || !node.isConnected) return false\n    node.focus()\n    return document.activeElement === node\n  }\n\n  /**\n   * Fields to hand the caret to on the next commit, in the order they are read. Deferred rather\n   * than focused in place because the commit that reports an error is also the commit that\n   * re-enables the fields — aiming before it lands is aiming at a disabled control.\n   */\n  const focusRequestRef = React.useRef<readonly CheckoutField[] | null>(null)\n\n  React.useLayoutEffect(() => {\n    const wanted = focusRequestRef.current\n    if (wanted === null) return\n    focusRequestRef.current = null\n    // Walk the list until something takes focus, rather than aiming once and losing the keyboard.\n    for (const field of fieldOrder) {\n      if (!wanted.includes(field)) continue\n      if (focusField(field)) return\n    }\n  })\n\n  /**\n   * The one write path. `valuesRef` is updated synchronously alongside the state because a submit\n   * or a blur can land before React has committed the edit that caused it.\n   */\n  const patch = (partial: Partial<CheckoutFormValues>) => {\n    setRaw(previous => ({ ...previous, ...partial }))\n    valuesRef.current = { ...valuesRef.current, ...partial }\n  }\n\n  const setValue = (field: CheckoutField, next: string) => {\n    if (valuesRef.current[field] === next) return\n    // Every `CheckoutField` holds a `string`, so the computed key is sound; TypeScript widens a\n    // union-typed key to an index signature and cannot see that on its own.\n    patch({ [field]: next } as Partial<CheckoutFormValues>)\n    // Typing never raises an error and it clears the one showing: submit is the only moment that\n    // reports. Returning `previous` untouched keeps this from re-rendering.\n    setErrors(previous => (previous[field] === undefined ? previous : { ...previous, [field]: undefined }))\n  }\n\n  const setMethod = (id: CheckoutPaymentMethodId) => {\n    if (valuesRef.current.method === id) return\n    // Errors were written against the previous method's field set (\"CVC is required\" on a form\n    // that no longer has a CVC), so none of them survive the switch.\n    setErrors({})\n    patch({ method: id })\n  }\n\n  const setCountry = (code: string) => {\n    const current = valuesRef.current\n    if (current.country === code) return\n    const next = resolveCheckoutCountry(code, countries)\n    const partial: Partial<CheckoutFormValues> = { country: code }\n    // A subdivision code is country-specific — \"CA\" is California in the US and nothing at all in\n    // Canada — so a value survives only if the new country's own list contains it.\n    const options = next.region?.options\n    if (current.region !== \"\" && !(options !== undefined && options.some(o => o.value === current.region))) {\n      partial.region = \"\"\n    }\n    // A code that still validates is kept: switching country to fix a typo should not make you\n    // retype an address that is still correct.\n    const postal = current.postalCode.trim()\n    if (postal !== \"\" && !next.postalCode.pattern.test(postal.toUpperCase())) partial.postalCode = \"\"\n    // Errors were written against the old country's rules (\"ZIP code is required\" on a form that\n    // no longer has a ZIP code), so none of them survive the switch either.\n    setErrors({})\n    patch(partial)\n  }\n\n  const brand = detectCardBrand(values.cardNumber)\n  const cardNumberDisplay = groupDigits(values.cardNumber, brand.groups)\n  const cardNumberMaxLength = groupDigits(\"0\".repeat(Math.max(...brand.lengths)), brand.groups).length\n  const cvcMaxLength = Math.max(...brand.cvcLengths)\n  const cvcHintLength = Math.min(...brand.cvcLengths)\n\n  /** One accepted edit → one clamped value, one formatted display, one caret. */\n  const applyDigits = (field: DigitField, rawInput: string, digitsBefore: number, el: HTMLInputElement) => {\n    let digits: string\n    let display: string\n    if (field === \"cardExpiry\") {\n      digits = normalizeExpiryDigits(rawInput)\n      display = formatExpiry(digits)\n    } else if (field === \"cardCvc\") {\n      digits = rawInput.replace(/\\D/g, \"\").slice(0, Math.max(...detectCardBrand(values.cardNumber).cvcLengths))\n      display = digits\n    } else {\n      const typed = rawInput.replace(/\\D/g, \"\")\n      digits = typed.slice(0, Math.max(...detectCardBrand(typed).lengths))\n      display = groupDigits(digits, detectCardBrand(digits).groups)\n    }\n    // The month pad inserts a leading 0 (\"5\" → \"05\"), so a caret behind the typed digit has to\n    // step over it — otherwise the next keystroke lands inside the month and reorders it.\n    const padded =\n      field === \"cardExpiry\" && digits.startsWith(\"0\") && (rawInput.match(/\\d/)?.[0] ?? \"0\") !== \"0\"\n    const kept = Math.min(digitsBefore + (padded && digitsBefore > 0 ? 1 : 0), digits.length)\n    const caret = caretAfterDigits(display, kept)\n    // Write the conformed text before React re-renders: the controlled update then finds the DOM\n    // already holding the final string and never reassigns `value` — that reassignment is exactly\n    // what drops the caret at the end of the field.\n    el.value = display\n    el.setSelectionRange(caret, caret)\n    caretRef.current = { el, caret }\n    setValue(field, digits)\n  }\n\n  const handleDigitChange = (field: DigitField, event: React.ChangeEvent<HTMLInputElement>) => {\n    const el = event.currentTarget\n    const caret = el.selectionStart ?? el.value.length\n    applyDigits(field, el.value, countDigits(el.value.slice(0, caret)), el)\n  }\n\n  const handleDigitKeyDown = (field: DigitField, event: React.KeyboardEvent<HTMLInputElement>) => {\n    const backwards = event.key === \"Backspace\"\n    if ((!backwards && event.key !== \"Delete\") || event.altKey || event.ctrlKey || event.metaKey) return\n    const el = event.currentTarget\n    const start = el.selectionStart ?? 0\n    if (start !== (el.selectionEnd ?? 0)) return\n    // The character this key is aimed at is a separator we drew ourselves. Removing only that\n    // would be a no-op — the next render puts it straight back — so the keystroke reaches over it\n    // and eats the digit on the far side. Both directions: a Delete that only ever re-deletes the\n    // same regenerated space never moves at all.\n    if (isDigitChar(backwards ? el.value[start - 1] : el.value[start])) return\n    event.preventDefault()\n    const digits = el.value.replace(/\\D/g, \"\")\n    const index = countDigits(el.value.slice(0, start)) - (backwards ? 1 : 0)\n    if (index < 0 || index >= digits.length) return\n    applyDigits(field, digits.slice(0, index) + digits.slice(index + 1), index, el)\n  }\n\n  // Every control takes the native `disabled` attribute while the payment is in flight, and the\n  // browser blurs a control the instant it becomes disabled — so someone who pressed Enter inside\n  // a field would be dumped on <body> with the pending state announced from nowhere. The pay\n  // button is only ever `aria-disabled`, which keeps it focusable, so it can take the handover.\n  React.useLayoutEffect(() => {\n    if (status !== \"submitting\") return\n    const active = document.activeElement\n    if (active !== null && active !== document.body) return\n    submitRef.current?.focus()\n  }, [status])\n\n  // Backstop for the commits where React does rewrite `value` (a brand switch that regroups the\n  // digits): put the caret back before paint, so it is never seen sitting at the end of the field.\n  // DOM writes only, no setState.\n  React.useLayoutEffect(() => {\n    const pending = caretRef.current\n    caretRef.current = null\n    if (pending === null) return\n    const { caret, el } = pending\n    if (!el.isConnected || document.activeElement !== el) return\n    const target = Math.min(caret, el.value.length)\n    if (el.selectionStart !== target || el.selectionEnd !== target) el.setSelectionRange(target, target)\n  })\n\n  /* ---------------------------------------------------------------------- */\n  /* Submit                                                                 */\n  /* ---------------------------------------------------------------------- */\n\n  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {\n    event.preventDefault()\n    // The guard is read and written synchronously, before anything can yield. Five presses inside\n    // one tick reach this line one after another and only the first gets past it.\n    if (pendingRef.current || disabled) return\n    if (cartEmpty) {\n      setFailure(copy.emptyCartFailure)\n      setStatus(\"error\")\n      return\n    }\n\n    const current = valuesRef.current\n    const schema = buildCheckoutSchema({ country, method: method.id, now })\n    const parsed = schema.safeParse(current)\n    if (!parsed.success) {\n      const next: Partial<Record<CheckoutField, string>> = {}\n      for (const issue of parsed.error.issues) {\n        const key = issue.path[0]\n        if (typeof key !== \"string\") continue\n        const field = key as CheckoutField\n        if (fieldOrder.includes(field) && next[field] === undefined) next[field] = issue.message\n      }\n      setErrors(next)\n      // A field problem supersedes an earlier network failure: this press never reached the\n      // processor, so leaving \"the gateway is down\" next to \"that isn't an email address\" would\n      // blame the wrong thing.\n      setFailure(null)\n      setStatus(\"idle\")\n      focusRequestRef.current = Object.keys(next) as CheckoutField[]\n      return\n    }\n\n    const data = parsed.data\n    const address: CheckoutAddress = {\n      city: data.city.trim(),\n      country: data.country,\n      line1: data.line1.trim(),\n      name: data.name.trim(),\n      postalCode: data.postalCode.trim(),\n    }\n    if (data.line2.trim() !== \"\") address.line2 = data.line2.trim()\n    if (country?.region !== undefined && data.region.trim() !== \"\") address.region = data.region.trim()\n\n    let payment: CheckoutPaymentDetails\n    if (method.id === \"card\") {\n      const reference = now ?? new Date()\n      payment = {\n        brand: detectCardBrand(data.cardNumber).id,\n        cvc: data.cardCvc,\n        expiryMonth: Number(data.cardExpiry.slice(0, 2)),\n        expiryYear: expandYear(Number(data.cardExpiry.slice(2, 4)), reference),\n        last4: data.cardNumber.slice(-4),\n        method: \"card\",\n        name: data.cardName.trim(),\n        number: data.cardNumber,\n      }\n    } else if (method.id === \"invoice\") {\n      payment = { company: data.company.trim(), method: \"invoice\", taxId: data.taxId.trim() }\n    } else {\n      payment = { method: \"wallet\" }\n    }\n\n    pendingRef.current = true\n    setErrors({})\n    setFailure(null)\n    setStatus(\"submitting\")\n\n    // `new Promise(resolve => resolve(...))` rather than `Promise.resolve(fn())`: an onSubmit that\n    // throws synchronously escapes the latter before it is wrapped, and the button stays pending\n    // for ever.\n    new Promise<void>(resolve => {\n      resolve(onSubmitRef.current({ email: data.email, payment, shippingAddress: address, totals }))\n    }).then(\n      () => {\n        if (!mountedRef.current) return\n        // The guard stays engaged on purpose: a payment that succeeded must not be submittable a\n        // second time. The consumer navigates away; remount to start another order.\n        setStatus(\"success\")\n      },\n      error => {\n        pendingRef.current = false\n        // Re-check liveness after the await: a late rejection landing on an unmounted instance\n        // would setState on a dead component.\n        if (!mountedRef.current) return\n        const attached = fieldErrorsOf(error)\n        // Nothing is cleared. Not the card number, not the address: a declined payment is usually\n        // retried with the same details, and wiping the form is how a customer gives up.\n        setErrors(attached ?? {})\n        setFailure(rejectionText(error, copy.failureFallback))\n        setStatus(\"error\")\n        // With a field to blame, the caret goes there on the next commit. Without one it stays on\n        // the pay button, which carries `aria-describedby` to the banner — so the reason is read\n        // out at the control the customer is about to press again, instead of being announced\n        // from a paragraph they then have to find their way back out of.\n        if (attached !== undefined) focusRequestRef.current = Object.keys(attached) as CheckoutField[]\n      },\n    )\n  }\n\n  /* ---------------------------------------------------------------------- */\n  /* Render                                                                 */\n  /* ---------------------------------------------------------------------- */\n\n  const busy = status === \"submitting\"\n  const describedBy = (field: CheckoutField, hasHint = false) =>\n    errors[field] !== undefined ? errorId(field) : hasHint ? hintId(field) : undefined\n\n  const controlProps = (field: CheckoutField) => ({\n    \"aria-describedby\": describedBy(field),\n    \"aria-invalid\": errors[field] !== undefined || undefined,\n    className: CONTROL_CLASS,\n    disabled: disabled || busy || status === \"success\",\n    id: fieldId(field),\n  })\n\n  const textInput = (\n    field: CheckoutField,\n    ref: React.RefObject<HTMLInputElement | null>,\n    autoComplete: string,\n    extra?: { inputMode?: \"text\" | \"numeric\" | \"email\"; maxLength?: number; placeholder?: string; type?: string },\n  ) => (\n    <input\n      {...controlProps(field)}\n      autoComplete={autoComplete}\n      inputMode={extra?.inputMode}\n      maxLength={extra?.maxLength}\n      onChange={event => setValue(field, event.currentTarget.value)}\n      placeholder={extra?.placeholder}\n      ref={ref}\n      type={extra?.type ?? \"text\"}\n      value={values[field]}\n    />\n  )\n\n  const renderAddressField = (field: CheckoutAddressField): React.ReactNode => {\n    switch (field) {\n      case \"line1\":\n        return (\n          <Field\n            className=\"@[30rem]:col-span-2\"\n            controlId={fieldId(\"line1\")}\n            error={errors.line1}\n            errorId={errorId(\"line1\")}\n            hintId={hintId(\"line1\")}\n            key=\"line1\"\n            label={country.line1Label}\n          >\n            {textInput(\"line1\", line1Ref, \"shipping address-line1\")}\n          </Field>\n        )\n      case \"line2\":\n        return (\n          <Field\n            className=\"@[30rem]:col-span-2\"\n            controlId={fieldId(\"line2\")}\n            error={errors.line2}\n            errorId={errorId(\"line2\")}\n            hintId={hintId(\"line2\")}\n            key=\"line2\"\n            label={country.line2Label}\n          >\n            {textInput(\"line2\", line2Ref, \"shipping address-line2\")}\n          </Field>\n        )\n      case \"city\":\n        return (\n          <Field\n            className=\"@[30rem]:col-span-2\"\n            controlId={fieldId(\"city\")}\n            error={errors.city}\n            errorId={errorId(\"city\")}\n            hintId={hintId(\"city\")}\n            key=\"city\"\n            label={country.cityLabel}\n          >\n            {textInput(\"city\", cityRef, \"shipping address-level2\")}\n          </Field>\n        )\n      case \"region\": {\n        const rule = country.region\n        if (rule === undefined) return null\n        return (\n          <Field\n            controlId={fieldId(\"region\")}\n            error={errors.region}\n            errorId={errorId(\"region\")}\n            hintId={hintId(\"region\")}\n            key=\"region\"\n            label={rule.label}\n          >\n            {rule.options === undefined ? (\n              textInput(\"region\", regionInputRef, \"shipping address-level1\")\n            ) : (\n              <div className=\"relative flex min-w-0 items-center\">\n                <select\n                  {...controlProps(\"region\")}\n                  autoComplete=\"shipping address-level1\"\n                  className={cn(CONTROL_CLASS, SELECT_CLASS)}\n                  onChange={event => setValue(\"region\", event.currentTarget.value)}\n                  ref={regionSelectRef}\n                  value={values.region}\n                >\n                  <option value=\"\">{copy.selectPlaceholder(rule.label)}</option>\n                  {rule.options.map(option => (\n                    <option key={option.value} value={option.value}>\n                      {option.label}\n                    </option>\n                  ))}\n                </select>\n                <ChevronDown aria-hidden=\"true\" className=\"pointer-events-none absolute right-3 size-4 text-muted-foreground\" />\n              </div>\n            )}\n          </Field>\n        )\n      }\n      case \"postalCode\":\n        return (\n          <Field\n            controlId={fieldId(\"postalCode\")}\n            error={errors.postalCode}\n            errorId={errorId(\"postalCode\")}\n            hintId={hintId(\"postalCode\")}\n            key=\"postalCode\"\n            label={country.postalCode.label}\n          >\n            {textInput(\"postalCode\", postalCodeRef, \"shipping postal-code\", {\n              inputMode: country.postalCode.inputMode,\n              maxLength: country.postalCode.maxLength,\n              placeholder: country.postalCode.example,\n            })}\n          </Field>\n        )\n    }\n  }\n\n  const summaryRows: React.ReactNode[] = [\n    <SummaryRow key=\"subtotal\" label={copy.subtotalLabel} slot=\"subtotal\" value={money(totals.subtotal)} />,\n    <SummaryRow\n      badge={totals.shipping === 0 && !cartEmpty ? copy.freeBadge : undefined}\n      key=\"shipping\"\n      label={totals.shippingLabel}\n      slot=\"shipping\"\n      value={money(totals.shipping)}\n    />,\n  ]\n  if (totals.fee > 0) {\n    summaryRows.push(\n      <SummaryRow key=\"fee\" label={totals.feeLabel ?? \"Payment fee\"} slot=\"fee\" value={money(totals.fee)} />,\n    )\n  }\n  summaryRows.push(<SummaryRow key=\"tax\" label={totals.taxLabel} slot=\"tax\" value={money(totals.tax)} />)\n  if (totals.discount > 0) {\n    summaryRows.push(\n      <SummaryRow\n        key=\"discount\"\n        label={totals.discountLabel ?? \"Discount\"}\n        slot=\"discount\"\n        // The sign comes from Intl, not from a hand-written \"-\": some locales bracket negatives.\n        value={money(-totals.discount)}\n      />,\n    )\n  }\n\n  const payLabel = method.submitLabel ?? `${copy.payLabel} ${money(totals.total)}`\n\n  return (\n    <form\n      className={cn(\"@container w-full\", className)}\n      noValidate\n      onSubmit={handleSubmit}\n      ref={forwardedRef}\n      {...rest}\n    >\n      <div className=\"grid items-start gap-8 @[52rem]:grid-cols-[minmax(0,1fr)_20rem]\">\n        <div className=\"flex min-w-0 flex-col gap-8\">\n          {/* Contact ------------------------------------------------------ */}\n          <section className=\"flex flex-col gap-1\">\n            <h3 className=\"text-sm font-semibold text-foreground\">{copy.contactHeading}</h3>\n            <p className=\"mb-2 text-xs text-muted-foreground\">{copy.contactNote}</p>\n            <Field\n              controlId={fieldId(\"email\")}\n              error={errors.email}\n              errorId={errorId(\"email\")}\n              hintId={hintId(\"email\")}\n              label={copy.emailLabel}\n            >\n              {textInput(\"email\", emailRef, \"email\", {\n                inputMode: \"email\",\n                placeholder: copy.emailPlaceholder,\n                type: \"email\",\n              })}\n            </Field>\n          </section>\n\n          {/* Delivery address --------------------------------------------- */}\n          <section className=\"flex flex-col gap-2\">\n            <h3 className=\"text-sm font-semibold text-foreground\">{copy.shippingHeading}</h3>\n            <div className=\"grid grid-cols-1 gap-x-4 gap-y-1 @[30rem]:grid-cols-2\">\n              <Field\n                className=\"@[30rem]:col-span-2\"\n                controlId={fieldId(\"country\")}\n                error={errors.country}\n                errorId={errorId(\"country\")}\n                hintId={hintId(\"country\")}\n                label={copy.countryLabel}\n              >\n                <div className=\"relative flex min-w-0 items-center\">\n                  <select\n                    {...controlProps(\"country\")}\n                    // \"country\" is the ISO-code token; \"country-name\" is for a field holding\n                    // \"United States\". This select's value is the code.\n                    autoComplete=\"shipping country\"\n                    className={cn(CONTROL_CLASS, SELECT_CLASS)}\n                    onChange={event => setCountry(event.currentTarget.value)}\n                    ref={countryRef}\n                    value={values.country}\n                  >\n                    {/* A code the table does not list stays visible instead of rendering blank. */}\n                    {countries.some(entry => entry.code === values.country) ? null : (\n                      <option value={values.country}>{values.country}</option>\n                    )}\n                    {countries.map(entry => (\n                      <option key={entry.code} value={entry.code}>\n                        {entry.name}\n                      </option>\n                    ))}\n                  </select>\n                  <ChevronDown aria-hidden=\"true\" className=\"pointer-events-none absolute right-3 size-4 text-muted-foreground\" />\n                </div>\n              </Field>\n\n              <Field\n                className=\"@[30rem]:col-span-2\"\n                controlId={fieldId(\"name\")}\n                error={errors.name}\n                errorId={errorId(\"name\")}\n                hintId={hintId(\"name\")}\n                label={copy.nameLabel}\n              >\n                {textInput(\"name\", nameRef, \"shipping name\", { placeholder: copy.namePlaceholder })}\n              </Field>\n\n              {addressOrder.map(field => renderAddressField(field))}\n            </div>\n          </section>\n\n          {/* Payment ------------------------------------------------------ */}\n          <section className=\"flex flex-col gap-2\">\n            <h3 className=\"text-sm font-semibold text-foreground\">{copy.paymentHeading}</h3>\n            <fieldset className=\"min-w-0 border-0 p-0\">\n              <legend className=\"sr-only\">{copy.paymentHeading}</legend>\n              <div className=\"grid gap-2\">\n                {methodList.map(entry => {\n                  const Icon = METHOD_ICONS[entry.id]\n                  const active = entry.id === method.id\n                  return (\n                    <label\n                      className={cn(\n                        \"flex min-w-0 cursor-pointer items-start gap-3 rounded-lg border p-3 transition-colors motion-reduce:transition-none\",\n                        active ? \"border-primary bg-primary/5\" : \"hover:bg-muted\",\n                        (disabled || busy || status === \"success\") && \"cursor-not-allowed opacity-60\",\n                      )}\n                      key={entry.id}\n                    >\n                      <input\n                        checked={active}\n                        className=\"mt-0.5 size-4 shrink-0 accent-primary\"\n                        disabled={disabled || busy || status === \"success\"}\n                        name={`${uid}-method`}\n                        onChange={() => setMethod(entry.id)}\n                        type=\"radio\"\n                        value={entry.id}\n                      />\n                      <Icon aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0 text-muted-foreground\" />\n                      <span className=\"flex min-w-0 flex-col gap-0.5\">\n                        <span className=\"text-sm font-medium text-foreground\">{entry.label}</span>\n                        <span className=\"text-xs text-muted-foreground [overflow-wrap:anywhere]\">\n                          {entry.description}\n                        </span>\n                      </span>\n                    </label>\n                  )\n                })}\n              </div>\n            </fieldset>\n\n            {method.id === \"card\" && (\n              <div className=\"grid grid-cols-1 gap-x-4 gap-y-1 pt-2 @[30rem]:grid-cols-2\">\n                <Field\n                  className=\"@[30rem]:col-span-2\"\n                  controlId={fieldId(\"cardNumber\")}\n                  error={errors.cardNumber}\n                  errorId={errorId(\"cardNumber\")}\n                  hintId={hintId(\"cardNumber\")}\n                  label={copy.cardNumberLabel}\n                >\n                  <div className=\"relative\">\n                    <input\n                      {...controlProps(\"cardNumber\")}\n                      autoComplete=\"cc-number\"\n                      className={cn(CONTROL_CLASS, \"pr-20 font-mono tabular-nums\")}\n                      inputMode=\"numeric\"\n                      maxLength={cardNumberMaxLength}\n                      onChange={event => handleDigitChange(\"cardNumber\", event)}\n                      onKeyDown={event => handleDigitKeyDown(\"cardNumber\", event)}\n                      placeholder=\"1234 1234 1234 1234\"\n                      ref={cardNumberRef}\n                      spellCheck={false}\n                      value={cardNumberDisplay}\n                    />\n                    {/* A wordmark, not a logo: the networks license their marks, and a text badge\n                        themes for free. Decorative — the brand reaches assistive tech through the\n                        live region at the foot of the form, which carries no digits. */}\n                    <span\n                      aria-hidden=\"true\"\n                      className=\"pointer-events-none absolute inset-y-0 right-2 flex items-center\"\n                    >\n                      {brand.wordmark === \"\" ? (\n                        <CreditCard className=\"size-5 text-muted-foreground/70\" strokeWidth={1.75} />\n                      ) : (\n                        <span className=\"rounded border bg-muted px-1.5 py-0.5 text-[10px] font-semibold tracking-tight text-muted-foreground\">\n                          {brand.wordmark}\n                        </span>\n                      )}\n                    </span>\n                  </div>\n                </Field>\n\n                <Field\n                  controlId={fieldId(\"cardExpiry\")}\n                  error={errors.cardExpiry}\n                  errorId={errorId(\"cardExpiry\")}\n                  hintId={hintId(\"cardExpiry\")}\n                  label={copy.cardExpiryLabel}\n                >\n                  <input\n                    {...controlProps(\"cardExpiry\")}\n                    autoComplete=\"cc-exp\"\n                    className={cn(CONTROL_CLASS, \"font-mono tabular-nums\")}\n                    inputMode=\"numeric\"\n                    maxLength={5}\n                    onChange={event => handleDigitChange(\"cardExpiry\", event)}\n                    onKeyDown={event => handleDigitKeyDown(\"cardExpiry\", event)}\n                    placeholder=\"MM/YY\"\n                    ref={cardExpiryRef}\n                    spellCheck={false}\n                    value={formatExpiry(values.cardExpiry)}\n                  />\n                </Field>\n\n                <Field\n                  controlId={fieldId(\"cardCvc\")}\n                  error={errors.cardCvc}\n                  errorId={errorId(\"cardCvc\")}\n                  hint={`${cvcHintLength} digits on the ${brand.id === \"amex\" ? \"front\" : \"back\"}`}\n                  hintId={hintId(\"cardCvc\")}\n                  label={brand.cvcLabel}\n                >\n                  <input\n                    {...controlProps(\"cardCvc\")}\n                    aria-describedby={describedBy(\"cardCvc\", true)}\n                    autoComplete=\"cc-csc\"\n                    className={cn(CONTROL_CLASS, \"font-mono tabular-nums\")}\n                    inputMode=\"numeric\"\n                    maxLength={cvcMaxLength}\n                    onChange={event => handleDigitChange(\"cardCvc\", event)}\n                    onKeyDown={event => handleDigitKeyDown(\"cardCvc\", event)}\n                    // The security code is the one value that must never arrive from somewhere\n                    // else: pasting it means it was stored somewhere it may not be, and a paste\n                    // into a mistyped field leaves it in the clipboard history either way.\n                    // Autofill from the browser's own card store still works — that is not a paste.\n                    onPaste={event => event.preventDefault()}\n                    placeholder={\"1234\".slice(0, cvcHintLength)}\n                    ref={cardCvcRef}\n                    spellCheck={false}\n                    value={values.cardCvc}\n                  />\n                </Field>\n\n                <Field\n                  className=\"@[30rem]:col-span-2\"\n                  controlId={fieldId(\"cardName\")}\n                  error={errors.cardName}\n                  errorId={errorId(\"cardName\")}\n                  hintId={hintId(\"cardName\")}\n                  label={copy.cardNameLabel}\n                >\n                  {textInput(\"cardName\", cardNameRef, \"cc-name\", { placeholder: copy.namePlaceholder })}\n                </Field>\n              </div>\n            )}\n\n            {method.id === \"wallet\" && (\n              <p className=\"rounded-lg border bg-muted px-3 py-2 text-xs leading-relaxed text-muted-foreground\">\n                {copy.walletNote}\n              </p>\n            )}\n\n            {method.id === \"invoice\" && (\n              <div className=\"grid grid-cols-1 gap-x-4 gap-y-1 pt-2 @[30rem]:grid-cols-2\">\n                <p className=\"rounded-lg border bg-muted px-3 py-2 text-xs leading-relaxed text-muted-foreground @[30rem]:col-span-2\">\n                  {copy.invoiceNote}\n                </p>\n                <Field\n                  controlId={fieldId(\"company\")}\n                  error={errors.company}\n                  errorId={errorId(\"company\")}\n                  hintId={hintId(\"company\")}\n                  label={copy.companyLabel}\n                >\n                  {textInput(\"company\", companyRef, \"organization\", { placeholder: \"Northwind Traders\" })}\n                </Field>\n                <Field\n                  controlId={fieldId(\"taxId\")}\n                  error={errors.taxId}\n                  errorId={errorId(\"taxId\")}\n                  hintId={hintId(\"taxId\")}\n                  label={copy.taxIdLabel}\n                >\n                  {textInput(\"taxId\", taxIdRef, \"off\", { placeholder: \"GB123456789\" })}\n                </Field>\n              </div>\n            )}\n          </section>\n        </div>\n\n        {/* Summary ---------------------------------------------------------- */}\n        <section\n          aria-labelledby={summaryHeadingId}\n          className=\"flex min-w-0 flex-col gap-4 rounded-xl border bg-card p-4 text-card-foreground @[52rem]:sticky @[52rem]:top-4\"\n        >\n          <h3 className=\"text-sm font-semibold\" id={summaryHeadingId}>\n            {copy.summaryHeading}\n          </h3>\n\n          {cartEmpty ? (\n            <div className=\"flex flex-col gap-1 rounded-lg border border-dashed px-3 py-6 text-center\">\n              <p className=\"text-sm font-medium\">{copy.emptyCart}</p>\n              <p className=\"text-xs text-muted-foreground\">{copy.emptyCartHint}</p>\n            </div>\n          ) : (\n            <ul className=\"flex flex-col gap-3\">\n              {lines.map(({ item, lineTotal, quantity, unitAmount }) => (\n                <li className=\"flex min-w-0 items-start justify-between gap-3 text-sm\" key={item.id}>\n                  <span className=\"flex min-w-0 flex-col gap-0.5\">\n                    <span className=\"font-medium [overflow-wrap:anywhere]\">{item.name}</span>\n                    {item.variant !== undefined && (\n                      <span className=\"text-xs text-muted-foreground [overflow-wrap:anywhere]\">{item.variant}</span>\n                    )}\n                    <span className=\"text-xs text-muted-foreground tabular-nums\">\n                      {copy.quantityLabel(quantity)}\n                      {quantity > 1 ? ` · ${money(unitAmount)} each` : \"\"}\n                    </span>\n                  </span>\n                  <span className=\"shrink-0 tabular-nums\">{money(lineTotal)}</span>\n                </li>\n              ))}\n            </ul>\n          )}\n\n          <dl className=\"flex flex-col gap-2 border-t pt-4 text-sm text-muted-foreground\">\n            {summaryRows}\n            <SummaryRow\n              className=\"mt-1 border-t pt-3 text-base\"\n              emphasis\n              label={copy.totalLabel}\n              slot=\"total\"\n              value={money(totals.total)}\n            />\n          </dl>\n\n          <p className=\"text-xs leading-relaxed text-muted-foreground\">{copy.estimateNote}</p>\n\n          {failure !== null && (\n            <div\n              className=\"flex items-start gap-2 rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs leading-relaxed text-foreground\"\n              id={failureId}\n              role=\"alert\"\n            >\n              <CircleAlert aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0 text-destructive\" />\n              <span className=\"[overflow-wrap:anywhere]\">{failure}</span>\n            </div>\n          )}\n\n          {status === \"success\" ? (\n            <div\n              className=\"flex items-start gap-2 rounded-lg border bg-muted px-3 py-3 text-sm\"\n              role=\"status\"\n            >\n              <CircleCheck aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0 text-muted-foreground\" />\n              <span className=\"flex flex-col gap-0.5\">\n                <span className=\"font-medium\">{copy.successTitle}</span>\n                <span className=\"text-xs text-muted-foreground\">{copy.successBody}</span>\n              </span>\n            </div>\n          ) : (\n            <button\n              // aria-disabled rather than the native attribute: the browser blurs a control the\n              // instant it becomes disabled, so someone who pressed Enter would be dumped on\n              // <body> with the pending state announced from nowhere. The real guard is the ref\n              // read at the top of the handler, which no number of presses can get past.\n              aria-busy={busy || undefined}\n              aria-describedby={failure !== null ? failureId : undefined}\n              aria-disabled={busy || disabled || cartEmpty || undefined}\n              className={cn(\n                \"inline-flex h-10 w-full items-center justify-center gap-2 rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow-xs\",\n                \"outline-none transition-colors motion-reduce:transition-none hover:bg-primary/90\",\n                \"focus-visible:ring-3 focus-visible:ring-ring/50\",\n                (busy || disabled || cartEmpty) && \"cursor-not-allowed opacity-60 hover:bg-primary\",\n              )}\n              ref={submitRef}\n              type=\"submit\"\n            >\n              {busy && <LoaderCircle aria-hidden=\"true\" className=\"size-4 animate-spin motion-reduce:animate-none\" />}\n              {busy ? copy.pendingLabel : payLabel}\n            </button>\n          )}\n\n          <p className=\"text-center text-xs text-muted-foreground\">{copy.securityNote}</p>\n        </section>\n      </div>\n\n      {/* One polite region for the whole form. The card wordmark is decorative, so this is how the\n          detected brand reaches a screen reader — by name and code length only. No digits are\n          ever put here, or anywhere else outside the number field itself. */}\n      <span aria-live=\"polite\" className=\"sr-only\">\n        {method.id === \"card\" && brand.id !== \"unknown\" ? `${brand.label}, ${brand.cvcLabel} ${cvcHintLength} digits` : \"\"}\n      </span>\n    </form>\n  )\n})\n\nexport default CheckoutForm\n",
      "type": "registry:block"
    }
  ],
  "type": "registry:block"
}
