{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "order-confirmation",
  "title": "Order Confirmation",
  "description": "A post-checkout receipt block — a dictatable, one-click-copyable order number, an itemised table whose money is refined to add up, a delivery window rather than a promised day, ship / pickup / digital fulfillment, and print styles that keep it on paper.",
  "dependencies": [
    "zod",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.zyeon.ai/r/use-copy-to-clipboard.json"
  ],
  "files": [
    {
      "path": "src/registry/blocks/order-confirmation.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  AlertCircle,\n  ArrowRight,\n  CheckCircle2,\n  Copy,\n  Download,\n  Mail,\n  PackageCheck,\n  Printer,\n  SearchX,\n  Store,\n  Truck,\n  XCircle,\n} from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { useCopyToClipboard } from \"@/hooks/use-copy-to-clipboard\"\nimport type { Order, OrderConfirmationData, PostalAddress } from \"./order-confirmation.contract\"\n\n/* ------------------------------------------------------------------ constants */\n\nconst BUTTON_BASE =\n  \"inline-flex cursor-pointer items-center gap-1.5 rounded-lg text-sm font-medium transition-colors \" +\n  \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none motion-reduce:transition-none\"\n\nconst PRIMARY_BUTTON = `${BUTTON_BASE} bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90`\nconst GHOST_BUTTON = `${BUTTON_BASE} border px-4 py-2 hover:bg-muted`\nconst SMALL_BUTTON = `${BUTTON_BASE} border px-2.5 py-1 text-xs hover:bg-muted`\n\n/** Headline per lifecycle state — a cancelled order must never wear the success wording. */\nconst STATE_TITLE: Record<Order[\"state\"], string> = {\n  placed: \"Order confirmed\",\n  processing: \"We're preparing your order\",\n  shipped: \"Your order is on its way\",\n  delivered: \"Your order was delivered\",\n  cancelled: \"Order cancelled\",\n}\n\n/* ------------------------------------------------------------------ formatting */\n\n/**\n * One currency's formatter. Two things this must never do: hard-code a `/100`\n * divisor (JPY has no minor unit, KWD has three digits), and hand-build the\n * string from a symbol plus `toFixed(2)` (in `de-DE` the symbol trails the\n * number, and where the minus sign of a discount goes is a locale decision).\n */\nfunction buildMoney(locale: string, currency: string): (minorUnits: number) => string {\n  for (const tag of [locale, \"en-US\"]) {\n    try {\n      const format = new Intl.NumberFormat(tag, { currency, style: \"currency\" })\n      const divisor = 10 ** (format.resolvedOptions().maximumFractionDigits ?? 2)\n      // Normalising -0 keeps a zero discount printing \"$0.00\", never \"-$0.00\".\n      return minorUnits => format.format(minorUnits === 0 ? 0 : minorUnits / divisor)\n    } catch {\n      // Malformed BCP-47 tag or currency code — `Intl` throws RangeError; try the next fallback.\n    }\n  }\n  // Neither the tag nor the code was usable. Show digits plus the raw code rather\n  // than silently relabelling the money as some other currency.\n  const format = new Intl.NumberFormat(\"en-US\", { maximumFractionDigits: 2, minimumFractionDigits: 2 })\n  return minorUnits => `${format.format(minorUnits / 100)} ${currency}`\n}\n\nfunction buildDateFormat(locale: string, timeZone: string): Intl.DateTimeFormat {\n  for (const tag of [locale, \"en-US\"]) {\n    try {\n      return new Intl.DateTimeFormat(tag, { dateStyle: \"medium\", timeZone })\n    } catch {\n      // Bad tag or unknown zone — `Intl` throws RangeError; fall through to UTC.\n    }\n  }\n  return new Intl.DateTimeFormat(\"en-US\", { dateStyle: \"medium\", timeZone: \"UTC\" })\n}\n\nfunction buildNumberFormat(locale: string): Intl.NumberFormat {\n  try {\n    return new Intl.NumberFormat(locale)\n  } catch {\n    return new Intl.NumberFormat(\"en-US\")\n  }\n}\n\n/** Epoch ms, or null — a bad instant must render nothing rather than \"Invalid Date\". */\nfunction toMs(iso: string | undefined): number | null {\n  if (!iso) return null\n  const ms = Date.parse(iso)\n  return Number.isFinite(ms) ? ms : null\n}\n\n/**\n * Which calendar day an instant falls on **in the given zone**, as a day index.\n * Never `getDate()` on the visitor's clock: 2026-08-04T23:30Z is August 4 in\n * London and August 5 in Tokyo, so \"arrives in 2 days\" would differ per traveller\n * for one and the same carrier promise.\n */\nfunction dayIndexIn(ms: number, dayParts: Intl.DateTimeFormat): number | null {\n  const parts = dayParts.formatToParts(new Date(ms))\n  const read = (type: Intl.DateTimeFormatPartTypes) => {\n    const value = parts.find(part => part.type === type)?.value\n    const parsed = value === undefined ? Number.NaN : Number.parseInt(value, 10)\n    return Number.isFinite(parsed) ? parsed : null\n  }\n  const year = read(\"year\")\n  const month = read(\"month\")\n  const day = read(\"day\")\n  if (year === null || month === null || day === null) return null\n  return Math.round(Date.UTC(year, month - 1, day) / 86_400_000)\n}\n\nfunction buildDayParts(timeZone: string): Intl.DateTimeFormat {\n  try {\n    return new Intl.DateTimeFormat(\"en-US\", { day: \"2-digit\", month: \"2-digit\", timeZone, year: \"numeric\" })\n  } catch {\n    return new Intl.DateTimeFormat(\"en-US\", { day: \"2-digit\", month: \"2-digit\", timeZone: \"UTC\", year: \"numeric\" })\n  }\n}\n\n/**\n * A delivery promise is a *window*, so it is formatted as one. `formatRange`\n * collapses shared parts by itself (\"Aug 4 – 6, 2026\") and knows where the dash\n * goes in the target locale; hand-joining two formatted dates with an en dash\n * gets both wrong.\n */\nfunction formatRange(format: Intl.DateTimeFormat, fromMs: number, untilMs: number): string {\n  if (typeof format.formatRange === \"function\") {\n    try {\n      return format.formatRange(new Date(fromMs), new Date(untilMs))\n    } catch {\n      // Some engines throw when the two dates are identical or reversed.\n    }\n  }\n  const from = format.format(new Date(fromMs))\n  const until = format.format(new Date(untilMs))\n  return from === until ? from : `${from} – ${until}`\n}\n\nfunction plural(count: number, noun: string, format: Intl.NumberFormat) {\n  return `${format.format(count)} ${noun}${count === 1 ? \"\" : \"s\"}`\n}\n\n/**\n * Groups a compact order number into blocks that can be read down a phone line\n * (\"7QK4 M2XP 9T\"). The separator is a **margin, not a character**: nothing is\n * inserted into the text, so selecting the number by hand and copying it yields\n * exactly the stored value — the same string the copy button writes. A number\n * that already carries its own punctuation (\"ORD-2026-0042\") is left alone;\n * re-chunking a value that is already structured makes it harder to read, not\n * easier.\n */\nfunction groupOrderNumber(raw: string, size = 4): string[] {\n  // Floored and required to be >= 2: a group of one is not a grouping, it is one\n  // margin per character (\"7 Q K 4 …\"), and a fractional step would make the\n  // slices overlap and print characters twice.\n  const step = Math.floor(size)\n  if (!Number.isFinite(step) || step < 2 || !/^[0-9A-Za-z]+$/.test(raw) || raw.length <= step) return [raw]\n  const groups: string[] = []\n  for (let i = 0; i < raw.length; i += step) groups.push(raw.slice(i, i + step))\n  return groups\n}\n\n/* ------------------------------------------------------------------ pieces */\n\nfunction AddressLines({\n  address,\n  className,\n  slot,\n}: {\n  address: PostalAddress\n  className?: string\n  /** \"shipping-address\" for the customer's destination, \"pickup-address\" for the branch. */\n  slot: string\n}) {\n  const region = [address.city, address.region, address.postalCode].filter(Boolean).join(\", \")\n  return (\n    <p className={cn(\"min-w-0 text-sm wrap-anywhere text-muted-foreground\", className)} data-slot={slot}>\n      <span className=\"block font-medium text-foreground\">{address.name}</span>\n      <span className=\"block\">{address.line1}</span>\n      {address.line2 && <span className=\"block\">{address.line2}</span>}\n      {region && <span className=\"block\">{region}</span>}\n      <span className=\"block\">{address.country}</span>\n      {address.phone && <span className=\"block\">{address.phone}</span>}\n    </p>\n  )\n}\n\nfunction Panel({ children, className }: { children: React.ReactNode; className?: string }) {\n  return (\n    <div className={cn(\"flex flex-col items-center gap-2 rounded-xl border bg-card px-6 py-14 text-center\", className)}>\n      {children}\n    </div>\n  )\n}\n\nfunction SkeletonBar({ className }: { className?: string }) {\n  return <div className={cn(\"h-3 animate-pulse rounded bg-muted motion-reduce:animate-none\", className)} />\n}\n\n/* ------------------------------------------------------------------ component */\n\nexport interface OrderConfirmationProps\n  extends OrderConfirmationData,\n    Omit<React.ComponentPropsWithoutRef<\"section\">, \"children\" | keyof OrderConfirmationData> {\n  /** BCP-47 tag driving money, dates and quantities. Explicit so server and client agree. */\n  locale?: string\n  /** IANA zone the dates are read in — the customer's, not the browser's. Default \"UTC\". */\n  timeZone?: string\n  /**\n   * The current instant, ISO 8601. Injected rather than read from the clock so a\n   * server render and a client render produce identical text and the demo is\n   * reproducible. Omit it and the estimate simply prints its dates with no\n   * \"arrives in N days\" phrasing — nothing is invented.\n   */\n  now?: string\n  /** Replaces the state-derived headline. */\n  heading?: React.ReactNode\n  /** Digits per group in the displayed order number. 0 or 1 disables grouping. Default 4. */\n  groupSize?: number\n  /** Shown in the error state only; omit and no retry control is rendered. */\n  onRetry?: () => void\n  /** Overrides the Print action. Defaults to `window.print()`. */\n  onPrint?: () => void\n}\n\nexport const OrderConfirmation = React.forwardRef<HTMLElement, OrderConfirmationProps>(function OrderConfirmation(\n  {\n    status,\n    order,\n    locale = \"en-US\",\n    timeZone = \"UTC\",\n    now,\n    heading,\n    groupSize = 4,\n    onRetry,\n    onPrint,\n    className,\n    ...rest\n  },\n  ref,\n) {\n  const reactId = React.useId()\n  const headingId = `${reactId}-heading`\n  const numberLabelId = `${reactId}-number`\n  const itemsHeadingId = `${reactId}-items`\n  const totalsHeadingId = `${reactId}-totals`\n  const fulfillmentHeadingId = `${reactId}-fulfillment`\n\n  const { copied, copy } = useCopyToClipboard()\n  const [selectFallback, setSelectFallback] = React.useState(false)\n  const numberRef = React.useRef<HTMLOutputElement>(null)\n\n  const money = React.useMemo(() => buildMoney(locale, order?.currency ?? \"USD\"), [locale, order?.currency])\n  const dateFormat = React.useMemo(() => buildDateFormat(locale, timeZone), [locale, timeZone])\n  const dayParts = React.useMemo(() => buildDayParts(timeZone), [timeZone])\n  const numberFormat = React.useMemo(() => buildNumberFormat(locale), [locale])\n\n  const rawNumber = order?.number ?? \"\"\n\n  const handleCopy = React.useCallback(() => {\n    void copy(rawNumber).then(copiedOk => {\n      if (copiedOk) {\n        setSelectFallback(false)\n        return\n      }\n      // Degradation, not a lie: the clipboard API is unavailable or was denied, so\n      // put the number under the caret and tell the reader to use the keyboard.\n      // Because the groups are separated by margins rather than characters, the\n      // selection serialises to the same string the button would have written.\n      setSelectFallback(true)\n      const node = numberRef.current\n      if (!node || typeof window === \"undefined\") return\n      const selection = window.getSelection()\n      if (!selection) return\n      const range = document.createRange()\n      range.selectNodeContents(node)\n      selection.removeAllRanges()\n      selection.addRange(range)\n    })\n  }, [copy, rawNumber])\n\n  const handlePrint = React.useCallback(() => {\n    if (onPrint) {\n      onPrint()\n      return\n    }\n    if (typeof window !== \"undefined\") window.print()\n  }, [onPrint])\n\n  const section = (children: React.ReactNode) => (\n    <section className={cn(\"w-full\", className)} ref={ref} {...rest}>\n      {children}\n    </section>\n  )\n\n  if (status === \"loading\") {\n    return section(\n      <div className=\"flex flex-col gap-6 rounded-xl border bg-card p-5 sm:p-6\">\n        <div className=\"flex flex-col gap-3\">\n          <SkeletonBar className=\"h-6 w-52\" />\n          <SkeletonBar className=\"w-72 max-w-full\" />\n          <SkeletonBar className=\"h-9 w-64 max-w-full\" />\n        </div>\n        <div className=\"flex flex-col gap-2.5\">\n          {Array.from({ length: 3 }, (_, index) => (\n            <div className=\"flex items-center gap-4\" key={index}>\n              <SkeletonBar className=\"h-4 min-w-0 flex-1\" />\n              <SkeletonBar className=\"h-4 w-8 shrink-0\" />\n              <SkeletonBar className=\"h-4 w-16 shrink-0\" />\n            </div>\n          ))}\n        </div>\n        <div className=\"flex flex-col gap-2\">\n          <SkeletonBar className=\"ms-auto w-40\" />\n          <SkeletonBar className=\"ms-auto h-4 w-28\" />\n        </div>\n        <p className=\"text-sm text-muted-foreground\">Loading your order…</p>\n      </div>,\n    )\n  }\n\n  if (status === \"error\") {\n    return section(\n      <Panel>\n        <AlertCircle aria-hidden=\"true\" className=\"size-6 text-destructive\" />\n        <p className=\"text-sm font-medium\">We couldn&apos;t load your order</p>\n        <p className=\"max-w-md text-sm text-muted-foreground\">\n          Your payment isn&apos;t affected — the confirmation email has the same details.\n        </p>\n        {onRetry && (\n          <button className={cn(SMALL_BUTTON, \"mt-2 print:hidden\")} onClick={onRetry} type=\"button\">\n            Try again\n          </button>\n        )}\n      </Panel>,\n    )\n  }\n\n  // \"ready with nothing\" degrades to the empty panel instead of rendering a\n  // receipt skeleton with blanks where the money should be.\n  if (status === \"empty\" || !order) {\n    return section(\n      <Panel>\n        <SearchX aria-hidden=\"true\" className=\"size-6 text-muted-foreground\" />\n        <p className=\"text-sm font-medium\">No order found</p>\n        <p className=\"max-w-md text-sm text-muted-foreground\">\n          This confirmation link may have expired, or the order number belongs to a different account.\n        </p>\n      </Panel>,\n    )\n  }\n\n  const { actions, fulfillment, lines, totals } = order\n  const cancelled = order.state === \"cancelled\"\n  const delivered = order.state === \"delivered\"\n  const groups = groupOrderNumber(rawNumber, groupSize)\n\n  const placedMs = toMs(order.placedAt)\n  const completedMs = toMs(order.completedAt)\n  const cancelledMs = toMs(order.cancelledAt)\n  const nowMs = toMs(now)\n\n  const StateIcon = cancelled ? XCircle : delivered ? PackageCheck : CheckCircle2\n  const subtitle = cancelled\n    ? (order.cancelReason ?? \"This order was cancelled and will not be delivered.\")\n    : delivered\n      ? \"Everything in this order has arrived.\"\n      : order.email\n        ? `We emailed your receipt to ${order.email}.`\n        : \"Keep your order number handy — it's how support finds this order.\"\n\n  // The window is only shown while it is still a promise: a delivered order has a\n  // date, and a cancelled one has nothing coming.\n  const promiseWindow =\n    cancelled || delivered\n      ? null\n      : fulfillment.method === \"ship\"\n        ? fulfillment.eta\n        : fulfillment.method === \"pickup\"\n          ? fulfillment.readyWindow\n          : null\n  const windowFromMs = toMs(promiseWindow?.earliest)\n  const windowToMs = toMs(promiseWindow?.latest)\n  const hasWindow = windowFromMs !== null && windowToMs !== null\n\n  let relativeEta: string | null = null\n  let etaLate = false\n  if (hasWindow && nowMs !== null) {\n    const today = dayIndexIn(nowMs, dayParts)\n    const from = dayIndexIn(windowFromMs, dayParts)\n    const to = dayIndexIn(windowToMs, dayParts)\n    if (today !== null && from !== null && to !== null) {\n      const daysToEarliest = from - today\n      const daysToLatest = to - today\n      if (daysToLatest < 0) {\n        relativeEta = \"The estimate has passed — check tracking or contact support\"\n        etaLate = true\n      } else if (daysToLatest === 0) {\n        relativeEta = fulfillment.method === \"pickup\" ? \"Ready today\" : \"Arriving today\"\n      } else if (daysToEarliest <= 0) {\n        relativeEta = `Within ${plural(daysToLatest, \"day\", numberFormat)}`\n      } else if (daysToEarliest === daysToLatest) {\n        relativeEta = `In ${plural(daysToLatest, \"day\", numberFormat)}`\n      } else {\n        relativeEta = `In ${numberFormat.format(daysToEarliest)}–${plural(daysToLatest, \"day\", numberFormat)}`\n      }\n    }\n  }\n\n  const itemCount = lines.reduce((sum, line) => sum + line.quantity, 0)\n  // A digital order was never shipped, so there is no delivery line to show. A\n  // pickup order shows it as an explicit zero with the reason — silence there\n  // reads as \"we forgot to add it\".\n  const shippingNote =\n    fulfillment.method === \"pickup\"\n      ? \"Store pickup\"\n      : totals.shipping === 0\n        ? \"Free\"\n        : null\n  const showShippingRow = fulfillment.method !== \"digital\"\n\n  // The hook's `copied` flag lingers for a couple of seconds after a successful\n  // write. A second attempt that FAILS inside that window must not keep showing\n  // \"Copied\" — that is reporting a success the visitor did not get — so the\n  // fallback outranks it and only clears when a write actually succeeds again.\n  const showCopied = copied && !selectFallback\n  const copyMessage = selectFallback\n    ? \"Clipboard unavailable — the number is selected, press your copy shortcut\"\n    : showCopied\n      ? \"Copied\"\n      : \"\"\n\n  return section(\n    <div\n      className=\"flex w-full flex-col rounded-xl border bg-card text-card-foreground\"\n      data-slot=\"order-confirmation\"\n      data-state={order.state}\n    >\n      <header className=\"flex flex-col gap-5 border-b p-5 sm:p-6\">\n        {/*\n          One live region, and nothing inside it ever mutates: a `role=\"status\"`\n          re-announces on ANY descendant change, so putting the copy button here\n          would re-read the whole confirmation every time someone copied the\n          number. Copy feedback gets its own small region further down.\n        */}\n        <div className=\"flex items-start gap-3 break-inside-avoid\" role=\"status\">\n          <StateIcon\n            aria-hidden=\"true\"\n            className={cn(\"mt-0.5 size-6 shrink-0\", cancelled ? \"text-destructive\" : \"text-primary\")}\n          />\n          <div className=\"flex min-w-0 flex-col gap-1\">\n            <h2 className=\"text-lg font-semibold tracking-tight\" id={headingId}>\n              {heading ?? STATE_TITLE[order.state]}\n            </h2>\n            <p className=\"min-w-0 text-sm wrap-anywhere text-muted-foreground\">{subtitle}</p>\n          </div>\n        </div>\n\n        <dl className=\"flex flex-wrap items-start gap-x-8 gap-y-4 rounded-lg border bg-muted/40 p-3 break-inside-avoid\">\n          <div className=\"flex min-w-0 flex-col gap-1\">\n            <dt className=\"text-xs font-medium tracking-wide text-muted-foreground uppercase\" id={numberLabelId}>\n              Order number\n            </dt>\n            <dd className=\"flex flex-wrap items-center gap-2\">\n              <output\n                aria-labelledby={numberLabelId}\n                aria-live=\"off\"\n                className=\"font-mono text-base font-semibold\"\n                data-order-number={rawNumber}\n                data-slot=\"order-number\"\n                ref={numberRef}\n              >\n                {groups.map((group, index) => (\n                  <span className={cn(\"inline-block\", index < groups.length - 1 && \"me-2\")} key={index}>\n                    {group}\n                  </span>\n                ))}\n              </output>\n              <button\n                className={cn(SMALL_BUTTON, \"print:hidden\")}\n                data-slot=\"copy-order-number\"\n                onClick={handleCopy}\n                type=\"button\"\n              >\n                <Copy aria-hidden=\"true\" className=\"size-3.5\" />\n                {showCopied ? \"Copied\" : \"Copy\"}\n              </button>\n            </dd>\n            <dd\n              className=\"max-w-xs text-xs text-muted-foreground print:hidden\"\n              data-slot=\"copy-feedback\"\n              role=\"status\"\n            >\n              {copyMessage}\n            </dd>\n          </div>\n\n          {placedMs !== null && (\n            <div className=\"flex flex-col gap-1\">\n              <dt className=\"text-xs font-medium tracking-wide text-muted-foreground uppercase\">Placed</dt>\n              <dd className=\"text-sm\">\n                <time dateTime={order.placedAt}>{dateFormat.format(new Date(placedMs))}</time>\n              </dd>\n            </div>\n          )}\n\n          {delivered && completedMs !== null && (\n            <div className=\"flex flex-col gap-1\">\n              <dt className=\"text-xs font-medium tracking-wide text-muted-foreground uppercase\">Delivered</dt>\n              <dd className=\"text-sm\">\n                <time dateTime={order.completedAt}>{dateFormat.format(new Date(completedMs))}</time>\n              </dd>\n            </div>\n          )}\n\n          {cancelled && cancelledMs !== null && (\n            <div className=\"flex flex-col gap-1\">\n              <dt className=\"text-xs font-medium tracking-wide text-muted-foreground uppercase\">Cancelled</dt>\n              <dd className=\"text-sm\">\n                <time dateTime={order.cancelledAt}>{dateFormat.format(new Date(cancelledMs))}</time>\n              </dd>\n            </div>\n          )}\n\n          {order.payment && (\n            <div className=\"flex min-w-0 flex-col gap-1\">\n              <dt className=\"text-xs font-medium tracking-wide text-muted-foreground uppercase\">Payment</dt>\n              <dd className=\"min-w-0 text-sm wrap-anywhere\">{order.payment}</dd>\n            </div>\n          )}\n        </dl>\n      </header>\n\n      <div className=\"grid gap-6 p-5 sm:p-6 lg:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)]\">\n        <div className=\"flex min-w-0 flex-col gap-4\">\n          <h3 className=\"text-sm font-semibold\" id={itemsHeadingId}>\n            {plural(itemCount, \"item\", numberFormat)}\n          </h3>\n\n          {/*\n            A real table: the itemised list is tabular data. Nothing inside it is\n            `sr-only` and no cell contains a block-level child — hidden helper text\n            and stray line breaks both come back out when the table is selected and\n            pasted into a spreadsheet.\n          */}\n          <table aria-labelledby={itemsHeadingId} className=\"w-full table-auto text-sm\" data-slot=\"line-items\">\n            <thead>\n              <tr className=\"border-b text-xs tracking-wide text-muted-foreground uppercase\">\n                <th className=\"py-2 pe-3 text-left font-medium\" scope=\"col\">\n                  Item\n                </th>\n                <th className=\"py-2 pe-3 text-right font-medium\" scope=\"col\">\n                  Qty\n                </th>\n                <th className=\"py-2 text-right font-medium\" scope=\"col\">\n                  Amount\n                </th>\n              </tr>\n            </thead>\n            <tbody>\n              {lines.map(line => (\n                <tr className=\"border-b align-top break-inside-avoid last:border-0\" data-slot=\"line-item\" key={line.id}>\n                  <th className=\"min-w-0 py-2.5 pe-3 text-left font-medium wrap-anywhere\" scope=\"row\">\n                    {line.name}\n                    {line.variant && <span className=\"font-normal text-muted-foreground\"> · {line.variant}</span>}\n                    {line.quantity > 1 && (\n                      <span className=\"font-normal text-muted-foreground\"> · {money(line.unitAmount)} each</span>\n                    )}\n                  </th>\n                  <td className=\"py-2.5 pe-3 text-right tabular-nums whitespace-nowrap text-muted-foreground\">\n                    {numberFormat.format(line.quantity)}\n                  </td>\n                  <td className=\"py-2.5 text-right font-medium tabular-nums whitespace-nowrap\" data-line-amount={line.amount}>\n                    {money(line.amount)}\n                  </td>\n                </tr>\n              ))}\n            </tbody>\n          </table>\n\n          <h3 className=\"sr-only\" id={totalsHeadingId}>\n            Payment summary\n          </h3>\n          <dl\n            aria-labelledby={totalsHeadingId}\n            className=\"ms-auto grid w-full max-w-xs grid-cols-[minmax(0,1fr)_auto] gap-x-6 gap-y-1.5 text-sm break-inside-avoid\"\n            data-slot=\"totals\"\n          >\n            <dt className=\"text-muted-foreground\">Subtotal</dt>\n            <dd className=\"text-right tabular-nums\" data-total=\"subtotal\">\n              {money(totals.subtotal)}\n            </dd>\n\n            {totals.discount > 0 && (\n              <>\n                <dt className=\"text-muted-foreground\">Discount</dt>\n                <dd className=\"text-right tabular-nums\" data-total=\"discount\">\n                  {money(-totals.discount)}\n                </dd>\n              </>\n            )}\n\n            {showShippingRow && (\n              <>\n                <dt className=\"min-w-0 wrap-anywhere text-muted-foreground\">\n                  Shipping\n                  {shippingNote && <span className=\"text-xs\"> · {shippingNote}</span>}\n                </dt>\n                <dd className=\"text-right tabular-nums\" data-total=\"shipping\">\n                  {money(totals.shipping)}\n                </dd>\n              </>\n            )}\n\n            <dt className=\"text-muted-foreground\">Tax</dt>\n            <dd className=\"text-right tabular-nums\" data-total=\"tax\">\n              {money(totals.tax)}\n            </dd>\n\n            <dt className=\"mt-1.5 border-t pt-2 font-semibold\">Total</dt>\n            <dd className=\"mt-1.5 border-t pt-2 text-right font-semibold tabular-nums\" data-total=\"total\">\n              {money(totals.total)}\n            </dd>\n          </dl>\n        </div>\n\n        <section\n          aria-labelledby={fulfillmentHeadingId}\n          className=\"flex min-w-0 flex-col gap-3 break-inside-avoid\"\n          data-slot=\"fulfillment\"\n          data-method={fulfillment.method}\n        >\n          <h3 className=\"flex items-center gap-2 text-sm font-semibold\" id={fulfillmentHeadingId}>\n            {fulfillment.method === \"ship\" && <Truck aria-hidden=\"true\" className=\"size-4 text-muted-foreground\" />}\n            {fulfillment.method === \"pickup\" && <Store aria-hidden=\"true\" className=\"size-4 text-muted-foreground\" />}\n            {fulfillment.method === \"digital\" && <Mail aria-hidden=\"true\" className=\"size-4 text-muted-foreground\" />}\n            {fulfillment.method === \"ship\" ? \"Shipping to\" : fulfillment.method === \"pickup\" ? \"Pick up at\" : \"Delivery\"}\n          </h3>\n\n          {fulfillment.method === \"ship\" && (\n            <AddressLines address={fulfillment.address} className=\"break-inside-avoid\" slot=\"shipping-address\" />\n          )}\n\n          {fulfillment.method === \"pickup\" && (\n            <>\n              <AddressLines address={fulfillment.location} className=\"break-inside-avoid\" slot=\"pickup-address\" />\n              {fulfillment.instructions && (\n                <p className=\"min-w-0 text-sm wrap-anywhere text-muted-foreground\">{fulfillment.instructions}</p>\n              )}\n            </>\n          )}\n\n          {/* No address block at all for a download — there is nowhere to send it. */}\n          {fulfillment.method === \"digital\" && (\n            <p className=\"min-w-0 text-sm wrap-anywhere text-muted-foreground\">\n              Your download links were sent to <span className=\"font-medium text-foreground\">{fulfillment.email}</span>.\n              Nothing will be shipped.\n            </p>\n          )}\n\n          {hasWindow && (\n            <div\n              className=\"flex flex-col gap-0.5 rounded-lg border bg-muted/40 px-3 py-2 break-inside-avoid\"\n              data-slot=\"eta\"\n            >\n              <span className=\"text-xs font-medium tracking-wide text-muted-foreground uppercase\">\n                {fulfillment.method === \"pickup\" ? \"Ready for pickup\" : \"Estimated delivery\"}\n              </span>\n              <span className=\"text-sm font-medium\" data-slot=\"eta-range\">\n                {formatRange(dateFormat, windowFromMs, windowToMs)}\n              </span>\n              {relativeEta && (\n                <span className={cn(\"text-xs\", etaLate ? \"text-destructive\" : \"text-muted-foreground\")}>\n                  {relativeEta}\n                </span>\n              )}\n              <span className=\"text-xs text-muted-foreground\">\n                Dates read in {timeZone} · a carrier estimate, not a guarantee\n              </span>\n            </div>\n          )}\n\n          {fulfillment.method === \"ship\" && fulfillment.tracking && (\n            <p className=\"min-w-0 text-sm wrap-anywhere\">\n              <span className=\"text-muted-foreground\">\n                {fulfillment.carrier ? `${fulfillment.carrier} tracking: ` : \"Tracking: \"}\n              </span>\n              <a\n                className=\"font-mono underline underline-offset-4 hover:text-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\n                href={fulfillment.tracking.url}\n              >\n                {fulfillment.tracking.code}\n              </a>\n            </p>\n          )}\n\n          {/*\n            Printed pages keep text that IS data (a tracking code) and drop text\n            that is only a control — \"Open your licence library\" means nothing on\n            paper without the URL behind it.\n          */}\n          {fulfillment.method === \"digital\" && fulfillment.access && (\n            <p className=\"min-w-0 text-sm wrap-anywhere print:hidden\">\n              <a\n                className=\"underline underline-offset-4 hover:text-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\n                href={fulfillment.access.href}\n              >\n                {fulfillment.access.label}\n              </a>\n            </p>\n          )}\n        </section>\n      </div>\n\n      {/* Controls that cannot act on paper are hidden when printing; the numbers stay. */}\n      <footer\n        className=\"flex flex-wrap items-center gap-3 border-t p-5 sm:p-6 print:hidden\"\n        data-slot=\"actions\"\n      >\n        <a className={PRIMARY_BUTTON} href={actions.viewOrder.href}>\n          {actions.viewOrder.label}\n          <ArrowRight aria-hidden=\"true\" className=\"size-4\" />\n        </a>\n\n        {actions.invoice?.state === \"ready\" && (\n          <a className={GHOST_BUTTON} href={actions.invoice.href}>\n            <Download aria-hidden=\"true\" className=\"size-4\" />\n            {actions.invoice.label}\n          </a>\n        )}\n        {actions.invoice?.state === \"pending\" && (\n          <p className=\"min-w-0 text-sm wrap-anywhere text-muted-foreground\">{actions.invoice.reason}</p>\n        )}\n\n        <button className={GHOST_BUTTON} onClick={handlePrint} type=\"button\">\n          <Printer aria-hidden=\"true\" className=\"size-4\" />\n          Print\n        </button>\n\n        <a\n          className=\"ms-auto rounded-sm text-sm underline-offset-4 hover:underline focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\n          href={actions.continueShopping.href}\n        >\n          {actions.continueShopping.label}\n        </a>\n      </footer>\n    </div>,\n  )\n})\n\nexport default OrderConfirmation\n",
      "type": "registry:block"
    },
    {
      "path": "src/registry/blocks/order-confirmation.contract.ts",
      "content": "import { z } from \"zod\"\n\n/**\n * Money rule for this whole contract: **every amount is an integer in the\n * currency's MINOR unit** — 4900 = $49.00, 128000 = ¥128,000 (the yen has no\n * minor unit), 12500 = 12.500 KWD (three digits). Never floats: this document is\n * a receipt for a charge that already happened, and 0.1 + 0.2 !== 0.3 is not an\n * acceptable answer to \"what did you take from my card?\". How many digits a\n * minor unit has is decided by `currency` through `Intl`, never hard-coded /100.\n *\n * **One order settles in exactly one currency.** There is a single `currency`\n * field and every amount below is in it, so the component can never be asked to\n * add two currencies into one total — converting would need a rate *and* the\n * date it was taken, and neither is in this contract. A basket that genuinely\n * charges in two currencies is two payments, and honestly two confirmations.\n */\n\n/**\n * A destination the visitor can actually open: an absolute `http(s)` URL, or a\n * root-relative path served by the host app. `\"#\"` and `\"\"` are rejected on\n * purpose — this block's whole job is the handful of things you do next, and a\n * \"Track your parcel\" link that goes nowhere is the exact defect this shape\n * exists to prevent.\n */\nexport const orderHrefSchema = z\n  .string()\n  .refine(value => /^https?:\\/\\/\\S+$/.test(value) || /^\\/\\S*$/.test(value), {\n    message: 'href must be an absolute http(s) URL or a root-relative path — \"#\" and \"\" are dead links',\n  })\n\n/**\n * Where the order is in its life, in the order it is walked:\n *\n * ```\n * placed ──▶ processing ──▶ shipped ──▶ delivered\n *   │            │             │\n *   └────────────┴─────────────┴──▶ cancelled\n * ```\n *\n * This page is reached once, right after checkout, so `placed` is the common\n * case — but the same URL is bookmarked, emailed and reopened weeks later, so\n * `delivered` and `cancelled` are first-class: a cancelled order rendered with a\n * green tick and a delivery estimate is a support call. Which state an order is\n * in is the commerce backend's decision (it knows the carrier scans and the\n * refund state); it is never derived in the browser from a clock.\n */\nexport const orderStateSchema = z.enum([\"placed\", \"processing\", \"shipped\", \"delivered\", \"cancelled\"])\n\n/**\n * A promise, not a fact: carriers commit to a *window*. Rendering one exact day\n * (\"arrives August 4\") converts an estimate into a guarantee the seller never\n * made, and every day of slack becomes a complaint. Both ends are absolute\n * instants (ISO 8601 with `Z` or a numeric offset) — which calendar day an\n * instant falls on is a time-zone decision, and the component makes that\n * decision explicitly through its `timeZone` prop instead of silently using the\n * visitor's machine.\n */\nexport const dateRangeSchema = z\n  .object({\n    earliest: z.iso.datetime({ offset: true }),\n    latest: z.iso.datetime({ offset: true }),\n  })\n  .superRefine((range, ctx) => {\n    // Guard first: zod runs every refinement, and a ragged payload must produce a\n    // validation error, not a TypeError thrown out of safeParse.\n    const from = typeof range?.earliest === \"string\" ? Date.parse(range.earliest) : Number.NaN\n    const to = typeof range?.latest === \"string\" ? Date.parse(range.latest) : Number.NaN\n    if (!Number.isFinite(from) || !Number.isFinite(to)) return\n    if (to < from) {\n      ctx.addIssue({\n        code: \"custom\",\n        message: \"latest must not be before earliest — a delivery window that ends before it starts is not a window\",\n        path: [\"latest\"],\n      })\n    }\n  })\n\n/** A postal address, printed one line per element so it survives copy/paste into a label. */\nexport const postalAddressSchema = z.object({\n  /** Recipient, or the branch name for a pickup point. */\n  name: z.string().min(1),\n  line1: z.string().min(1),\n  line2: z.string().optional(),\n  city: z.string().min(1),\n  /** State / province / prefecture — genuinely absent in many countries, so optional. */\n  region: z.string().optional(),\n  /** Optional: Ireland, Hong Kong and others have addresses without one. */\n  postalCode: z.string().optional(),\n  country: z.string().min(1),\n  phone: z.string().optional(),\n})\n\n/**\n * How the order reaches the customer. A discriminated union rather than a bag of\n * optional fields, because the three cases have genuinely different shapes: a\n * download has no street address to render, and a pickup has no carrier.\n * Modelling this as `address?` would let a digital order carry a half-filled\n * address and render an empty \"Shipping to\" card — the failure this shape\n * removes at the type level.\n */\nexport const fulfillmentSchema = z.discriminatedUnion(\"method\", [\n  z.object({\n    method: z.literal(\"ship\"),\n    address: postalAddressSchema,\n    /** The carrier's window. Absent when nothing has been promised yet, or the order was cancelled. */\n    eta: dateRangeSchema.optional(),\n    carrier: z.string().optional(),\n    /** Only rendered when there is something to open; the url is refined against dead links. */\n    tracking: z.object({ code: z.string().min(1), url: orderHrefSchema }).optional(),\n  }),\n  z.object({\n    method: z.literal(\"pickup\"),\n    /** The store, not the customer: `name` is the branch. */\n    location: postalAddressSchema,\n    /** When the parcel will be waiting — also a window, for the same reason. */\n    readyWindow: dateRangeSchema.optional(),\n    /** e.g. \"Bring photo ID and this order number.\" */\n    instructions: z.string().optional(),\n  }),\n  z.object({\n    method: z.literal(\"digital\"),\n    /** Where the licence / download link was sent. There is no address to render. */\n    email: z.email(),\n    /** Optional \"open your library\" destination. */\n    access: z.object({ label: z.string().min(1), href: orderHrefSchema }).optional(),\n  }),\n])\n\n/**\n * One purchased line. `amount` is carried rather than derived because it is what\n * the payment processor actually charged, and it is refined to equal\n * `quantity * unitAmount` so the two can never drift apart on screen. Per-line\n * promotions therefore belong in the order-level `discount`, not in a quietly\n * reduced `amount` — otherwise the receipt prints a unit price that multiplies\n * out to something else and leaves the customer doing arithmetic that fails.\n */\nexport const orderLineSchema = z\n  .object({\n    id: z.string().min(1),\n    name: z.string().min(1),\n    /** Size / colour / plan term — rendered inline after the name. */\n    variant: z.string().optional(),\n    quantity: z.number().int().positive(),\n    /** Price of one, in minor units. */\n    unitAmount: z.number().int().nonnegative(),\n    /** quantity × unitAmount, in minor units, as charged. */\n    amount: z.number().int().nonnegative(),\n  })\n  .superRefine((line, ctx) => {\n    const quantity = line?.quantity\n    const unitAmount = line?.unitAmount\n    const amount = line?.amount\n    if (!Number.isFinite(quantity) || !Number.isFinite(unitAmount) || !Number.isFinite(amount)) return\n    if (quantity * unitAmount !== amount) {\n      ctx.addIssue({\n        code: \"custom\",\n        message: `amount must equal quantity × unitAmount (${quantity} × ${unitAmount} = ${quantity * unitAmount}, got ${amount})`,\n        path: [\"amount\"],\n      })\n    }\n  })\n\n/**\n * The money breakdown. All five values are required — an optional `tax` reads as\n * \"no tax was charged\" and as \"we forgot to send it\" at the same time, and the\n * difference is a five-figure question in some jurisdictions. `discount` is a\n * **positive magnitude** that is subtracted; storing it negative means every\n * consumer has to remember which convention this codebase picked.\n */\nexport const orderTotalsSchema = z.object({\n  /** Σ of every line's `amount`. */\n  subtotal: z.number().int().nonnegative(),\n  /** Delivery charge. Exactly 0 for pickup and digital orders (refined below). */\n  shipping: z.number().int().nonnegative(),\n  tax: z.number().int().nonnegative(),\n  /** Positive magnitude; subtracted from the sum. */\n  discount: z.number().int().nonnegative(),\n  /** What was charged: subtotal + shipping + tax − discount, refined below. */\n  total: z.number().int().nonnegative(),\n})\n\nexport const orderSchema = z\n  .object({\n    /**\n     * The number a human reads out on the phone. Keep it exactly as the system\n     * stores it: the component groups it visually so it can be dictated, but\n     * grouping is a rendering decision and never enters this value. Copy always\n     * yields this string, character for character.\n     */\n    number: z.string().min(1),\n    /** Absolute instant the order was placed (ISO 8601 with Z or a numeric offset). */\n    placedAt: z.iso.datetime({ offset: true }),\n    state: orderStateSchema,\n    /** ISO 4217 alpha-3 — drives the symbol, its placement AND the minor-unit digits. */\n    currency: z.string().regex(/^[A-Za-z]{3}$/),\n    /** Where the confirmation email went, echoed back so a typo is caught while it still matters. */\n    email: z.email().optional(),\n    lines: z.array(orderLineSchema).min(1),\n    totals: orderTotalsSchema,\n    fulfillment: fulfillmentSchema,\n    /** Masked payment method, e.g. \"Visa ···· 4242\". Display only — never a full card number. */\n    payment: z.string().optional(),\n    /** When it reached the customer. Required by, and only allowed with, state \"delivered\". */\n    completedAt: z.iso.datetime({ offset: true }).optional(),\n    /** Required by, and only allowed with, state \"cancelled\". */\n    cancelledAt: z.iso.datetime({ offset: true }).optional(),\n    /** Shown with the cancelled banner, e.g. \"Refunded to your original payment method.\" */\n    cancelReason: z.string().optional(),\n    /**\n     * The things a person does from this page. `viewOrder` and `continueShopping`\n     * are required — a confirmation with no way onward is a dead end. `invoice`\n     * is three-way: omit it when no document exists, `pending` while the biller\n     * is still rendering one (its reason is printed as text), `ready` with a real\n     * href. There is deliberately no state that renders a control which cannot act.\n     */\n    actions: z.object({\n      viewOrder: z.object({ label: z.string().min(1), href: orderHrefSchema }),\n      invoice: z\n        .discriminatedUnion(\"state\", [\n          z.object({ state: z.literal(\"ready\"), label: z.string().min(1), href: orderHrefSchema }),\n          z.object({ state: z.literal(\"pending\"), reason: z.string().min(1) }),\n        ])\n        .optional(),\n      continueShopping: z.object({ label: z.string().min(1), href: orderHrefSchema }),\n    }),\n  })\n  .superRefine((order, ctx) => {\n    // Every branch guards its own inputs. zod runs all refinements, so a payload\n    // that already failed the shape check still reaches this function;\n    // dereferencing blindly here would throw a TypeError *out of* safeParse and\n    // leave the caller with no error object at all.\n    const totals = order?.totals\n    const lines = Array.isArray(order?.lines) ? order.lines : null\n    const int = (value: unknown): number | null =>\n      typeof value === \"number\" && Number.isFinite(value) ? value : null\n\n    const subtotal = int(totals?.subtotal)\n    const shipping = int(totals?.shipping)\n    const tax = int(totals?.tax)\n    const discount = int(totals?.discount)\n    const total = int(totals?.total)\n\n    // 1. The lines must add up to the subtotal, or the itemised list and the\n    //    breakdown are two different documents printed on the same page.\n    if (lines && subtotal !== null) {\n      let sum = 0\n      let complete = true\n      for (const line of lines) {\n        const amount = int(line?.amount)\n        if (amount === null) {\n          complete = false\n          break\n        }\n        sum += amount\n      }\n      if (complete && sum !== subtotal) {\n        ctx.addIssue({\n          code: \"custom\",\n          message: `subtotal must equal the sum of the line amounts (lines add to ${sum}, subtotal is ${subtotal})`,\n          path: [\"totals\", \"subtotal\"],\n        })\n      }\n    }\n\n    // 2. The breakdown must add up to what was charged.\n    if (subtotal !== null && shipping !== null && tax !== null && discount !== null && total !== null) {\n      const expected = subtotal + shipping + tax - discount\n      if (expected !== total) {\n        ctx.addIssue({\n          code: \"custom\",\n          message: `total must equal subtotal + shipping + tax − discount (${subtotal} + ${shipping} + ${tax} − ${discount} = ${expected}, got ${total})`,\n          path: [\"totals\", \"total\"],\n        })\n      }\n      if (discount > subtotal + shipping + tax) {\n        ctx.addIssue({\n          code: \"custom\",\n          message: \"discount cannot exceed subtotal + shipping + tax — an order never pays the customer\",\n          path: [\"totals\", \"discount\"],\n        })\n      }\n    }\n\n    // 3. Nothing was shipped, so nothing may be charged for shipping. A pickup\n    //    order carrying a delivery fee is a pricing bug, and this page is exactly\n    //    where a customer notices it.\n    const method = order?.fulfillment?.method\n    if (shipping !== null && shipping > 0 && (method === \"pickup\" || method === \"digital\")) {\n      ctx.addIssue({\n        code: \"custom\",\n        message: `a ${method} order cannot carry a shipping charge`,\n        path: [\"totals\", \"shipping\"],\n      })\n    }\n\n    // 4. The lifecycle timestamps and the state agree in both directions, so the\n    //    banner can never say \"Delivered\" with no delivery date, nor print a\n    //    cancellation date on a live order.\n    if (order?.state === \"delivered\" && !order.completedAt) {\n      ctx.addIssue({ code: \"custom\", message: 'state \"delivered\" requires completedAt', path: [\"completedAt\"] })\n    }\n    if (order?.completedAt && order.state !== \"delivered\") {\n      ctx.addIssue({ code: \"custom\", message: 'completedAt is only valid with state \"delivered\"', path: [\"state\"] })\n    }\n    if (order?.state === \"cancelled\" && !order.cancelledAt) {\n      ctx.addIssue({ code: \"custom\", message: 'state \"cancelled\" requires cancelledAt', path: [\"cancelledAt\"] })\n    }\n    if (order?.cancelledAt && order.state !== \"cancelled\") {\n      ctx.addIssue({ code: \"custom\", message: 'cancelledAt is only valid with state \"cancelled\"', path: [\"state\"] })\n    }\n  })\n\n/**\n * `status` is the *fetch* state of this page; `order.state` is where the parcel\n * is. Two different questions, never collapsed into one enum. `empty` means the\n * lookup succeeded and there is no such order — an expired confirmation link, or\n * an id that was never issued — which owes the reader a different sentence from\n * \"we couldn't reach the order service\".\n */\nexport const orderConfirmationSchema = z.object({\n  status: z.enum([\"loading\", \"empty\", \"error\", \"ready\"]),\n  order: orderSchema.optional(),\n})\n\nexport type OrderState = z.infer<typeof orderStateSchema>\nexport type DateRange = z.infer<typeof dateRangeSchema>\nexport type PostalAddress = z.infer<typeof postalAddressSchema>\nexport type Fulfillment = z.infer<typeof fulfillmentSchema>\nexport type OrderLine = z.infer<typeof orderLineSchema>\nexport type OrderTotals = z.infer<typeof orderTotalsSchema>\nexport type Order = z.infer<typeof orderSchema>\nexport type OrderConfirmationData = z.infer<typeof orderConfirmationSchema>\n",
      "type": "registry:block"
    }
  ],
  "type": "registry:block"
}
