{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "checkout-flow",
  "title": "Checkout Flow",
  "type": "registry:block",
  "description": "Three-step checkout (cart → payment → confirm) with animated step transitions, an inline PaymentForm, a success screen that fires a confetti burst with an animated checkmark, and an error screen with retry. Async submit is forwarded to the consumer-provided `onSubmit`, which returns an `orderId` that ends up on the success screen.",
  "categories": [
    "commerce"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/blocks/checkout-flow/checkout-flow.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { ArrowLeft, Minus, Plus, Trash2 } from 'lucide'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n// Side-effect imports: register the composed elements for standalone use\n// (consumers who install only this block still get working primitives).\nimport '../../components/button/button'\nimport '../../components/stepper/stepper'\nimport '../../components/separator/separator'\nimport '../../components/payment-card/payment-card'\nimport '../../components/input/input'\nimport type { UipInput } from '../../components/input/input'\nimport '../../components/label/label'\n\ntype CardBrand = 'visa' | 'mastercard' | 'amex' | 'discover' | 'unknown'\n\nexport interface PaymentPayload {\n  number: string\n  name: string\n  expiry: string\n  cvc: string\n  brand: CardBrand\n}\n\nexport interface CheckoutCartItem {\n  id: string\n  name: string\n  image?: string\n  qty: number\n  price: number\n}\n\ntype Step = 'cart' | 'payment' | 'confirm' | 'success' | 'error'\n\ninterface Particle {\n  id: number\n  x: number\n  y: number\n  vx: number\n  vy: number\n  rot: number\n  vr: number\n  color: string\n  size: number\n}\n\nfunction detectBrand(raw: string): CardBrand {\n  const n = raw.replace(/\\D/g, '')\n  if (!n) return 'unknown'\n  if (/^4/.test(n)) return 'visa'\n  if (/^(5[1-5]|2[2-7])/.test(n)) return 'mastercard'\n  if (/^3[47]/.test(n)) return 'amex'\n  if (/^(6011|65|64[4-9])/.test(n)) return 'discover'\n  return 'unknown'\n}\n\nfunction luhn(digits: string): boolean {\n  let sum = 0\n  let alt = false\n  for (let i = digits.length - 1; i >= 0; i--) {\n    let d = Number(digits[i])\n    if (alt) {\n      d *= 2\n      if (d > 9) d -= 9\n    }\n    sum += d\n    alt = !alt\n  }\n  return sum % 10 === 0 && digits.length >= 13\n}\n\nconst stepperSteps = [\n  { id: 'cart', title: 'Cart' },\n  { id: 'payment', title: 'Payment' },\n  { id: 'confirm', title: 'Confirm' },\n]\n\n/**\n * <uip-checkout-flow> — the registry CheckoutFlow block as a web component.\n *\n * Class strings, step keyframes, check-draw animation, and the confetti\n * physics are React's verbatim (keyframes are the one allowed extra-CSS\n * exception). React state becomes { state: true } fields; `items` is a\n * property (attribute: false — set it from JS). `onSubmit` (payload → orderId)\n * / `onComplete` ({ orderId }) / `onCancel` become `submit` / `complete` /\n * `cancel` CustomEvents (bubbles, composed; payloads in `detail`). Like\n * login-01, the block simulates the processor round-trip itself (1s) and then\n * notifies; `simulate-error` forces the rejection path so the demo can show it\n * without a callback prop.\n *\n * One deliberate departure: React embeds the PaymentForm *block* in the\n * payment step, but blocks never import sibling blocks (one block = one\n * install unit), so the payment step is an inline card form built from\n * uip-input + uip-payment-card with the same validation as uip-payment-form.\n */\nexport class UipCheckoutFlow extends LitElement {\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  // :host display can't be a utility class; step + check-draw keyframes are\n  // React's injected <style> copied verbatim (allowed keyframes exception).\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n      }\n      .checkout-flow-anim-forward,\n      .checkout-flow-anim-back {\n        animation-duration: 280ms;\n        animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n        animation-fill-mode: both;\n      }\n      .checkout-flow-anim-forward {\n        animation-name: checkout-flow-forward;\n      }\n      .checkout-flow-anim-back {\n        animation-name: checkout-flow-back;\n      }\n      @keyframes checkout-flow-forward {\n        from {\n          transform: translateX(24px);\n          opacity: 0;\n        }\n        to {\n          transform: translateX(0);\n          opacity: 1;\n        }\n      }\n      @keyframes checkout-flow-back {\n        from {\n          transform: translateX(-24px);\n          opacity: 0;\n        }\n        to {\n          transform: translateX(0);\n          opacity: 1;\n        }\n      }\n      .checkout-flow-check-draw {\n        stroke-dasharray: 100;\n        stroke-dashoffset: 100;\n        animation: checkout-flow-check-draw 700ms cubic-bezier(0.4, 0, 0.2, 1) 120ms forwards;\n      }\n      @keyframes checkout-flow-check-draw {\n        to {\n          stroke-dashoffset: 0;\n        }\n      }\n      @media (prefers-reduced-motion: reduce) {\n        .checkout-flow-anim-forward,\n        .checkout-flow-anim-back {\n          animation-duration: 0ms;\n        }\n        .checkout-flow-check-draw {\n          animation: none;\n          stroke-dashoffset: 0;\n        }\n      }\n    `,\n  ]\n\n  static properties = {\n    items: { attribute: false },\n    currency: {},\n    shippingFee: { type: Number, attribute: 'shipping-fee' },\n    taxRate: { type: Number, attribute: 'tax-rate' },\n    simulateError: { type: Boolean, attribute: 'simulate-error' },\n    step: { state: true },\n    direction: { state: true },\n    cart: { state: true },\n    paymentPayload: { state: true },\n    orderId: { state: true },\n    lastError: { state: true },\n    placing: { state: true },\n    particles: { state: true },\n    cardNumber: { state: true },\n    cardName: { state: true },\n    cardExpiry: { state: true },\n    cardCvc: { state: true },\n    cardErrors: { state: true },\n  }\n\n  items: CheckoutCartItem[] = []\n  currency = 'USD'\n  shippingFee = 0\n  taxRate = 0\n  simulateError = false\n\n  private step: Step = 'cart'\n  private direction: 'forward' | 'back' = 'forward'\n  private cart: CheckoutCartItem[] = []\n  private paymentPayload: PaymentPayload | null = null\n  private orderId: string | null = null\n  private lastError: Error | null = null\n  private placing = false\n  private particles: Particle[] = []\n  private cardNumber = ''\n  private cardName = ''\n  private cardExpiry = ''\n  private cardCvc = ''\n  private cardErrors: Record<string, string | null> = { number: null, name: null, expiry: null, cvc: null }\n  private confettiRaf: number | null = null\n  private timer?: number\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'checkout-flow')\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    if (this.confettiRaf) cancelAnimationFrame(this.confettiRaf)\n    if (this.timer) window.clearTimeout(this.timer)\n  }\n\n  willUpdate(changed: Map<string, unknown>) {\n    if (changed.has('items')) {\n      this.cart = (this.items ?? []).map((i) => ({ ...i }))\n    }\n  }\n\n  updated(changed: Map<string, unknown>) {\n    if (changed.has('step') && this.step === 'success') this.fireConfetti()\n  }\n\n  private go(next: Step) {\n    const order: Record<Step, number> = { cart: 0, payment: 1, confirm: 2, success: 3, error: 3 }\n    this.direction = order[next] >= order[this.step] ? 'forward' : 'back'\n    this.step = next\n  }\n\n  private get subtotal() {\n    return this.cart.reduce((s, i) => s + i.qty * i.price, 0)\n  }\n\n  private get tax() {\n    return this.subtotal * this.taxRate\n  }\n\n  private get total() {\n    return this.subtotal + this.shippingFee + this.tax\n  }\n\n  private fmt(n: number) {\n    try {\n      return new Intl.NumberFormat(undefined, { style: 'currency', currency: this.currency }).format(n)\n    } catch {\n      return `${this.currency} ${n.toFixed(2)}`\n    }\n  }\n\n  private get stepIndex() {\n    return this.step === 'cart' ? 1 : this.step === 'payment' ? 2 : 3\n  }\n\n  private dec(item: CheckoutCartItem) {\n    if (item.qty > 1) this.cart = this.cart.map((i) => (i.id === item.id ? { ...i, qty: i.qty - 1 } : i))\n  }\n\n  private inc(item: CheckoutCartItem) {\n    this.cart = this.cart.map((i) => (i.id === item.id ? { ...i, qty: i.qty + 1 } : i))\n  }\n\n  private removeItem(item: CheckoutCartItem) {\n    this.cart = this.cart.filter((i) => i.id !== item.id)\n  }\n\n  private get cardBrand(): CardBrand {\n    return detectBrand(this.cardNumber)\n  }\n\n  private formatCardNumber(raw: string, b: CardBrand): string {\n    const digits = raw.replace(/\\D/g, '').slice(0, b === 'amex' ? 15 : 16)\n    if (b === 'amex') {\n      return digits.replace(/^(\\d{0,4})(\\d{0,6})(\\d{0,5}).*$/, (_m, a, bb, c) => [a, bb, c].filter(Boolean).join(' '))\n    }\n    return digits.replace(/(.{4})/g, '$1 ').trim()\n  }\n\n  private formatCardExpiry(raw: string): string {\n    const d = raw.replace(/\\D/g, '').slice(0, 4)\n    if (d.length <= 2) return d\n    return `${d.slice(0, 2)}/${d.slice(2)}`\n  }\n\n  private validateCard(): boolean {\n    const digits = this.cardNumber.replace(/\\D/g, '')\n    const expectedLen = this.cardBrand === 'amex' ? 15 : 16\n    const next: Record<string, string | null> = { number: null, name: null, expiry: null, cvc: null }\n    next.number = !digits\n      ? 'Card number required'\n      : digits.length !== expectedLen\n        ? `Card number must be ${expectedLen} digits`\n        : !luhn(digits)\n          ? 'Card number is invalid'\n          : null\n    next.name = !this.cardName.trim() ? 'Name required' : this.cardName.trim().length < 2 ? 'Name too short' : null\n    const expOk = /^(0[1-9]|1[0-2])\\/\\d{2}$/.test(this.cardExpiry)\n    if (!expOk) {\n      next.expiry = 'Use MM/YY'\n    } else {\n      const [m, y] = this.cardExpiry.split('/').map(Number)\n      const now = new Date()\n      const expDate = new Date(2000 + (y ?? 0), (m ?? 1) - 1, 1)\n      expDate.setMonth(expDate.getMonth() + 1)\n      next.expiry = expDate <= now ? 'Card has expired' : null\n    }\n    const cvcLen = this.cardBrand === 'amex' ? 4 : 3\n    next.cvc = this.cardCvc.length !== cvcLen ? `CVC must be ${cvcLen} digits` : null\n    this.cardErrors = next\n    return Object.values(next).every((e) => !e)\n  }\n\n  private submitCardForm(e: Event) {\n    e.preventDefault()\n    if (!this.validateCard()) return\n    this.paymentPayload = {\n      number: this.cardNumber.replace(/\\D/g, ''),\n      name: this.cardName.trim(),\n      expiry: this.cardExpiry,\n      cvc: this.cardCvc,\n      brand: this.cardBrand,\n    }\n    this.go('confirm')\n  }\n\n  private placeOrder() {\n    if (!this.paymentPayload || this.placing) return\n    this.placing = true\n    this.timer = window.setTimeout(() => {\n      this.placing = false\n      if (this.simulateError) {\n        this.lastError = new Error('Issuer rejected the charge. Please contact your bank.')\n        this.go('error')\n        return\n      }\n      const nextOrderId = Math.random().toString(36).slice(2, 10).toUpperCase()\n      this.orderId = nextOrderId\n      this.dispatchEvent(\n        new CustomEvent('submit', { detail: { ...this.paymentPayload }, bubbles: true, composed: true }),\n      )\n      this.dispatchEvent(new CustomEvent('complete', { detail: { orderId: nextOrderId }, bubbles: true, composed: true }))\n      this.go('success')\n    }, 1000)\n  }\n\n  private fireConfetti() {\n    if (typeof window === 'undefined' || !window.matchMedia) return\n    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return\n    // Pull theme tokens at runtime so confetti follows the active palette\n    // (light/dark mode + any consumer theme override).\n    const cs = getComputedStyle(document.documentElement)\n    const tok = (name: string, fallback: string) => cs?.getPropertyValue(name).trim() || fallback\n    const colors = [\n      tok('--chart-1', 'oklch(0.65 0.18 145)'),\n      tok('--chart-2', 'oklch(0.6 0.22 260)'),\n      tok('--chart-3', 'oklch(0.7 0.16 75)'),\n      tok('--chart-4', 'oklch(0.65 0.2 330)'),\n      tok('--chart-5', 'oklch(0.6 0.22 30)'),\n      tok('--primary', 'oklch(0.55 0.22 260)'),\n    ]\n    this.particles = Array.from({ length: 60 }, (_, id) => ({\n      id,\n      x: 50,\n      y: 40,\n      vx: (Math.random() - 0.5) * 12,\n      vy: -Math.random() * 14 - 4,\n      rot: Math.random() * 360,\n      vr: (Math.random() - 0.5) * 20,\n      color: colors[Math.floor(Math.random() * colors.length)] ?? colors[0]!,\n      size: 4 + Math.random() * 6,\n    }))\n    const start = performance.now()\n    const tick = (now: number) => {\n      const elapsed = now - start\n      if (elapsed > 1800) {\n        this.particles = []\n        this.confettiRaf = null\n        return\n      }\n      this.particles = this.particles.map((p) => ({\n        ...p,\n        x: p.x + p.vx * 0.25,\n        y: p.y + p.vy * 0.25,\n        vy: p.vy + 0.45,\n        rot: p.rot + p.vr,\n      }))\n      this.confettiRaf = requestAnimationFrame(tick)\n    }\n    this.confettiRaf = requestAnimationFrame(tick)\n  }\n\n  render() {\n    const animClass = this.direction === 'forward' ? 'checkout-flow-anim-forward' : 'checkout-flow-anim-back'\n    return html`\n      <div data-slot=\"checkout-flow\" class=\"bg-card text-card-foreground mx-auto w-full max-w-3xl overflow-hidden rounded-xl border shadow-sm\">\n        ${this.step !== 'success' && this.step !== 'error'\n          ? html`<div class=\"bg-muted/30 border-b px-6 py-4\">\n              <uip-stepper .steps=${stepperSteps} value=${this.stepIndex}></uip-stepper>\n            </div>`\n          : nothing}\n\n        <div class=\"relative min-h-[420px]\">\n          ${this.step === 'cart' ? this.renderCart(animClass) : nothing}\n          ${this.step === 'payment' ? this.renderPayment(animClass) : nothing}\n          ${this.step === 'confirm' ? this.renderConfirm(animClass) : nothing}\n          ${this.step === 'success' ? this.renderSuccess(animClass) : nothing}\n          ${this.step === 'error' ? this.renderError(animClass) : nothing}\n        </div>\n      </div>\n    `\n  }\n\n  private renderCart(animClass: string) {\n    return html`<div class=${`p-6 ${animClass}`}>\n      <h3 class=\"mb-4 text-lg font-semibold\">Your cart</h3>\n      ${this.cart.length === 0\n        ? html`<div class=\"text-muted-foreground py-12 text-center text-sm\">Your cart is empty.</div>`\n        : html`<ul class=\"divide-border divide-y\">\n            ${this.cart.map(\n              (item) => html`<li class=\"flex items-center gap-3 py-3\">\n                <div class=\"bg-muted size-14 shrink-0 overflow-hidden rounded-md\">\n                  ${item.image ? html`<img src=${item.image} alt=${item.name} class=\"size-full object-cover\" />` : nothing}\n                </div>\n                <div class=\"min-w-0 flex-1\">\n                  <p class=\"truncate text-sm font-medium\">${item.name}</p>\n                  <p class=\"text-muted-foreground text-xs\">${this.fmt(item.price)} each</p>\n                </div>\n                <div class=\"flex items-center gap-1\">\n                  <uip-button variant=\"outline\" size=\"icon-sm\" aria-label=\"Decrease\" @click=${() => this.dec(item)}>\n                    ${icon(Minus, 'minus', 'size-3.5')}\n                  </uip-button>\n                  <span class=\"w-8 text-center text-sm tabular-nums\">${item.qty}</span>\n                  <uip-button variant=\"outline\" size=\"icon-sm\" aria-label=\"Increase\" @click=${() => this.inc(item)}>\n                    ${icon(Plus, 'plus', 'size-3.5')}\n                  </uip-button>\n                </div>\n                <div class=\"w-20 text-right text-sm font-medium tabular-nums\">${this.fmt(item.qty * item.price)}</div>\n                <uip-button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Remove\" @click=${() => this.removeItem(item)}>\n                  ${icon(Trash2, 'trash-2', 'text-muted-foreground size-4')}\n                </uip-button>\n              </li>`,\n            )}\n          </ul>`}\n      <uip-separator class=\"my-4\"></uip-separator>\n      <dl class=\"space-y-1.5 text-sm\">\n        <div class=\"flex justify-between\">\n          <dt class=\"text-muted-foreground\">Subtotal</dt>\n          <dd>${this.fmt(this.subtotal)}</dd>\n        </div>\n        ${this.shippingFee > 0\n          ? html`<div class=\"flex justify-between\">\n              <dt class=\"text-muted-foreground\">Shipping</dt>\n              <dd>${this.fmt(this.shippingFee)}</dd>\n            </div>`\n          : nothing}\n        ${this.taxRate > 0\n          ? html`<div class=\"flex justify-between\">\n              <dt class=\"text-muted-foreground\">Tax</dt>\n              <dd>${this.fmt(this.tax)}</dd>\n            </div>`\n          : nothing}\n        <div class=\"flex justify-between border-t pt-2 text-base font-semibold\">\n          <dt>Total</dt>\n          <dd>${this.fmt(this.total)}</dd>\n        </div>\n      </dl>\n      <div class=\"mt-5 flex justify-between\">\n        <uip-button\n          variant=\"ghost\"\n          @click=${() => this.dispatchEvent(new CustomEvent('cancel', { bubbles: true, composed: true }))}\n        >\n          Cancel\n        </uip-button>\n        <uip-button ?disabled=${this.cart.length === 0} @click=${() => this.go('payment')}>Continue to payment →</uip-button>\n      </div>\n    </div>`\n  }\n\n  private renderPayment(animClass: string) {\n    return html`<div class=${`p-6 ${animClass}`}>\n      <button\n        type=\"button\"\n        class=\"text-muted-foreground hover:text-foreground mb-3 inline-flex items-center gap-1 text-sm\"\n        @click=${() => this.go('cart')}\n      >\n        ${icon(ArrowLeft, 'arrow-left', 'size-3.5')} Back to cart\n      </button>\n      <div class=\"mb-4 flex justify-center\">\n        <uip-payment-card\n          number=${this.cardNumber}\n          name=${this.cardName}\n          expiry=${this.cardExpiry}\n          cvc=${this.cardCvc}\n          size=\"md\"\n        ></uip-payment-card>\n      </div>\n      <form class=\"space-y-3\" @submit=${(e: Event) => this.submitCardForm(e)}>\n        <div class=\"grid gap-1.5\">\n          <uip-label for=\"co-cc-number\">Card number</uip-label>\n          <uip-input\n            id=\"co-cc-number\"\n            type=\"text\"\n            autocomplete=\"cc-number\"\n            placeholder=\"1234 5678 9012 3456\"\n            .value=${this.cardNumber}\n            status=${this.cardErrors.number ? 'error' : nothing}\n            @input=${(e: Event) => {\n              const el = e.target as UipInput\n              const b = detectBrand(el.value ?? '')\n              this.cardNumber = this.formatCardNumber(el.value ?? '', b)\n              this.cardErrors = { ...this.cardErrors, number: null }\n            }}\n          ></uip-input>\n          ${this.cardErrors.number\n            ? html`<p class=\"text-destructive text-xs\" role=\"alert\">${this.cardErrors.number}</p>`\n            : nothing}\n        </div>\n        <div class=\"grid gap-1.5\">\n          <uip-label for=\"co-cc-name\">Name on card</uip-label>\n          <uip-input\n            id=\"co-cc-name\"\n            type=\"text\"\n            autocomplete=\"cc-name\"\n            placeholder=\"Jane Doe\"\n            .value=${this.cardName}\n            status=${this.cardErrors.name ? 'error' : nothing}\n            @input=${(e: Event) => {\n              this.cardName = (e.target as UipInput).value ?? ''\n            }}\n          ></uip-input>\n          ${this.cardErrors.name ? html`<p class=\"text-destructive text-xs\" role=\"alert\">${this.cardErrors.name}</p>` : nothing}\n        </div>\n        <div class=\"grid grid-cols-2 gap-3\">\n          <div class=\"grid gap-1.5\">\n            <uip-label for=\"co-cc-exp\">Expiry</uip-label>\n            <uip-input\n              id=\"co-cc-exp\"\n              type=\"text\"\n              autocomplete=\"cc-exp\"\n              placeholder=\"MM/YY\"\n              .value=${this.cardExpiry}\n              status=${this.cardErrors.expiry ? 'error' : nothing}\n              @input=${(e: Event) => {\n                this.cardExpiry = this.formatCardExpiry((e.target as UipInput).value ?? '')\n                this.cardErrors = { ...this.cardErrors, expiry: null }\n              }}\n            ></uip-input>\n            ${this.cardErrors.expiry\n              ? html`<p class=\"text-destructive text-xs\" role=\"alert\">${this.cardErrors.expiry}</p>`\n              : nothing}\n          </div>\n          <div class=\"grid gap-1.5\">\n            <uip-label for=\"co-cc-cvc\">CVC</uip-label>\n            <uip-input\n              id=\"co-cc-cvc\"\n              type=\"text\"\n              autocomplete=\"cc-csc\"\n              placeholder=${this.cardBrand === 'amex' ? '1234' : '123'}\n              .value=${this.cardCvc}\n              status=${this.cardErrors.cvc ? 'error' : nothing}\n              @input=${(e: Event) => {\n                const max = this.cardBrand === 'amex' ? 4 : 3\n                this.cardCvc = ((e.target as UipInput).value ?? '').replace(/\\D/g, '').slice(0, max)\n                this.cardErrors = { ...this.cardErrors, cvc: null }\n              }}\n            ></uip-input>\n            ${this.cardErrors.cvc ? html`<p class=\"text-destructive text-xs\" role=\"alert\">${this.cardErrors.cvc}</p>` : nothing}\n          </div>\n        </div>\n        <uip-button type=\"submit\" class=\"h-11 w-full text-base\">Pay ${this.fmt(this.total)}</uip-button>\n      </form>\n    </div>`\n  }\n\n  private renderConfirm(animClass: string) {\n    const p = this.paymentPayload\n    return html`<div class=${`p-6 ${animClass}`}>\n      <h3 class=\"mb-4 text-lg font-semibold\">Review your order</h3>\n      <div class=\"mb-4 flex items-start gap-4 rounded-lg border p-4\">\n        ${p\n          ? html`<uip-payment-card\n              number=${p.number}\n              name=${p.name}\n              expiry=${p.expiry}\n              brand=${p.brand}\n              variant=\"compact\"\n              flip=\"false\"\n            ></uip-payment-card>`\n          : nothing}\n        <div class=\"min-w-0 flex-1 text-sm\">\n          <p class=\"font-medium\">Paying with card ending ${p?.number.slice(-4)}</p>\n          <p class=\"text-muted-foreground\">${p?.name} · expires ${p?.expiry}</p>\n          <button class=\"text-primary mt-2 text-xs underline\" @click=${() => this.go('payment')}>\n            Change payment method\n          </button>\n        </div>\n      </div>\n      <ul class=\"divide-border divide-y border-y\">\n        ${this.cart.map(\n          (item) => html`<li class=\"flex justify-between py-2 text-sm\">\n            <span class=\"truncate\">${item.qty} × ${item.name}</span>\n            <span class=\"tabular-nums\">${this.fmt(item.qty * item.price)}</span>\n          </li>`,\n        )}\n      </ul>\n      <dl class=\"mt-3 space-y-1 text-sm\">\n        <div class=\"flex justify-between\">\n          <dt class=\"text-muted-foreground\">Subtotal</dt>\n          <dd>${this.fmt(this.subtotal)}</dd>\n        </div>\n        ${this.shippingFee > 0\n          ? html`<div class=\"flex justify-between\">\n              <dt class=\"text-muted-foreground\">Shipping</dt>\n              <dd>${this.fmt(this.shippingFee)}</dd>\n            </div>`\n          : nothing}\n        ${this.taxRate > 0\n          ? html`<div class=\"flex justify-between\">\n              <dt class=\"text-muted-foreground\">Tax</dt>\n              <dd>${this.fmt(this.tax)}</dd>\n            </div>`\n          : nothing}\n        <div class=\"flex justify-between border-t pt-2 text-base font-semibold\">\n          <dt>Total</dt>\n          <dd>${this.fmt(this.total)}</dd>\n        </div>\n      </dl>\n      <div class=\"mt-5 flex justify-between\">\n        <uip-button variant=\"outline\" @click=${() => this.go('payment')}>\n          ${icon(ArrowLeft, 'arrow-left', 'mr-1.5 size-4')} Back\n        </uip-button>\n        <uip-button ?disabled=${this.placing} @click=${() => this.placeOrder()}>\n          ${this.placing ? 'Placing order…' : `Place order · ${this.fmt(this.total)}`}\n        </uip-button>\n      </div>\n    </div>`\n  }\n\n  private renderSuccess(animClass: string) {\n    return html`<div class=${`relative flex flex-col items-center px-6 py-14 text-center ${animClass}`}>\n      <div class=\"pointer-events-none absolute inset-0 overflow-hidden\">\n        ${this.particles.map(\n          (pt) => html`<div\n            class=\"absolute\"\n            style=\"left:${pt.x}%;top:${pt.y}%;width:${pt.size}px;height:${pt.size}px;background:${pt.color};transform:rotate(${pt.rot}deg);border-radius:2px;\"\n          ></div>`,\n        )}\n      </div>\n      <div class=\"bg-primary/10 ring-primary/30 relative grid size-20 place-items-center rounded-full ring-2\">\n        <svg\n          viewBox=\"0 0 52 52\"\n          class=\"text-primary size-12\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          stroke-width=\"4\"\n          stroke-linecap=\"round\"\n          stroke-linejoin=\"round\"\n          aria-hidden=\"true\"\n        >\n          <path d=\"M14 27 L23 36 L40 18\" pathLength=\"100\" class=\"checkout-flow-check-draw\" />\n        </svg>\n      </div>\n      <h3 class=\"mt-5 text-xl font-semibold\">Order placed</h3>\n      <p class=\"text-muted-foreground mt-1 text-sm\">Thanks — a confirmation will be sent shortly.</p>\n      <div class=\"bg-muted mt-4 inline-flex rounded-md px-3 py-1.5 font-mono text-sm\">#${this.orderId}</div>\n      <uip-button\n        variant=\"outline\"\n        class=\"mt-6\"\n        @click=${() => this.dispatchEvent(new CustomEvent('cancel', { bubbles: true, composed: true }))}\n      >\n        Back to home\n      </uip-button>\n    </div>`\n  }\n\n  private renderError(animClass: string) {\n    return html`<div class=${`flex flex-col items-center px-6 py-14 text-center ${animClass}`}>\n      <div class=\"bg-destructive/10 ring-destructive/30 relative grid size-20 place-items-center rounded-full ring-2\">\n        <svg\n          viewBox=\"0 0 52 52\"\n          class=\"text-destructive size-12\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          stroke-width=\"4\"\n          stroke-linecap=\"round\"\n          aria-hidden=\"true\"\n        >\n          <path d=\"M16 16 L36 36 M36 16 L16 36\" pathLength=\"100\" class=\"checkout-flow-check-draw\" />\n        </svg>\n      </div>\n      <h3 class=\"mt-5 text-xl font-semibold\">Payment failed</h3>\n      <p class=\"text-muted-foreground mt-1 max-w-xs text-sm\">\n        ${this.lastError?.message || 'Something went wrong while charging your card.'}\n      </p>\n      <div class=\"mt-6 flex gap-2\">\n        <uip-button\n          variant=\"outline\"\n          @click=${() => this.dispatchEvent(new CustomEvent('cancel', { bubbles: true, composed: true }))}\n        >\n          Cancel\n        </uip-button>\n        <uip-button @click=${() => this.go('payment')}>Try again</uip-button>\n      </div>\n    </div>`\n  }\n}\n\ncustomElements.get('uip-checkout-flow') || customElements.define('uip-checkout-flow', UipCheckoutFlow)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-checkout-flow': UipCheckoutFlow\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/checkout-flow/checkout-flow.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/input.json",
    "https://uipkge.dev/r/lit/label.json",
    "https://uipkge.dev/r/lit/lit-core.json",
    "https://uipkge.dev/r/lit/payment-card.json",
    "https://uipkge.dev/r/lit/separator.json",
    "https://uipkge.dev/r/lit/stepper.json"
  ]
}
