{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "order-confirmation",
  "title": "Order Confirmation",
  "author": "ecomcn <hello@ecomcn.dev>",
  "description": "The page after Pay: the receipt people screenshot, print and come back to. A thank-you set large in your serif on a grained band, with a check that draws itself; the order number, placed date, arrival window and amount paid in one ruled row above the fold; the items as a ledger with totals; ship-to, delivery and payment. Prints as a plain black-on-white receipt with the permalink written out; Copy order link for a permalink you give it. Calendar dates in UTC so they never slip a day; the placed time in the visitor's zone once hydrated. Loading skeleton included. Compound, through useOrderConfirmation().",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "skeleton"
  ],
  "files": [
    {
      "path": "src/registry/ecomcn/order-confirmation/order-confirmation.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, Link2, Printer } from \"lucide-react\"\nimport { AnimatePresence, MotionConfig, motion, useReducedMotion } from \"motion/react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { cn } from \"@/lib/utils\"\n\n/*\n * The page after Pay: the receipt people screenshot, print and come back to.\n *\n *   <OrderConfirmation order={order} permalink={`https://shop.example/orders/${order.number}?t=${token}`} />\n *\n *   <OrderConfirmation order={order}>                         or composed\n *     <OrderConfirmationHeader />\n *     <OrderConfirmationFacts />\n *     <OrderConfirmationItems />\n *   </OrderConfirmation>\n *\n * The thank-you is set large in your serif on a grained band — the one\n * moment in the funnel that earns a flourish — with a check that draws\n * itself (Motion; none under reduced motion). Everything a shopper looks\n * for sits above the fold: the order number, the date, when it arrives,\n * what was paid. It prints as a plain receipt — no band, no buttons, black\n * on white, the permalink written out — and `permalink` is a link that\n * shows this order again from any device: give it a token, never session\n * state. Calendar dates (arrival) are shown in UTC so they never slip a\n * day; the placed time is the visitor's own once hydrated. A date range is\n * two formatted days joined with a dash, not `formatRange`: its spacing\n * differs between Node's ICU and the browser's, and the server's HTML has\n * to match the first client render exactly.\n */\n\nexport interface ConfirmationLine {\n  id: string\n  name: string\n  /** \"Tan · EU 42\". */\n  variant?: string\n  quantity: number\n  unitPrice: number\n  /** Already rendered, so the block never assumes next/image. */\n  image?: React.ReactNode\n}\n\nexport interface OrderConfirmationData {\n  /** What the shopper quotes to support: \"ATL-77760\". */\n  number: string\n  /** ISO timestamp the order was placed. */\n  placedAt: string\n  /** Where the confirmation email went. */\n  email?: string\n  lines: ConfirmationLine[]\n  totals: { subtotal: number; discount?: number; shipping: number; tax?: number; total: number }\n  /** The address as display lines — `formatAddressLines` from address-form gives these. */\n  shipTo?: string[]\n  delivery?: {\n    /** \"Express\". */\n    label: string\n    /** Calendar dates, \"2026-10-21\": the window it should arrive in. */\n    from?: string\n    to?: string\n  }\n  /** \"Visa ending 4242\", \"Pay in 3\". */\n  payment?: string\n}\n\n/** The same settle curve as the other ecomcn morphs: no bounce, an editorial ease. */\nconst MORPH = { duration: 0.45, ease: [0.2, 0, 0, 1] } as const\n\nconst subscribeNothing = () => () => {}\n\n/** True once hydrated: the server can't know the visitor's locale or time zone. */\nfunction useHydrated() {\n  return React.useSyncExternalStore(subscribeNothing, () => true, () => false)\n}\n\n/** Film grain as an inline SVG: no image request, and it prints as nothing. */\nconst GRAIN =\n  \"url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E\\\")\"\n\n/* ─── context ──────────────────────────────────────────────────────────── */\n\nexport interface OrderConfirmationContextValue {\n  order: OrderConfirmationData\n  formatMoney: (amount: number) => string\n  /** \"Wed, Oct 21\" — a calendar date, in UTC so it never slips a day. */\n  formatDay: (isoDate: string) => string\n  /** \"Wed, Oct 21 – Fri, Oct 23\", or one day when they match. */\n  formatWindow: (from: string, to?: string) => string\n  /** When it was placed: the visitor's date and time once hydrated, UTC before. */\n  formatPlaced: (isoTimestamp: string) => string\n  permalink?: string\n  title: React.ReactNode\n  actions?: React.ReactNode\n}\n\nconst OrderConfirmationContext = React.createContext<OrderConfirmationContextValue | null>(null)\n\nexport function useOrderConfirmation() {\n  const context = React.useContext(OrderConfirmationContext)\n  if (!context) {\n    throw new Error(\"useOrderConfirmation must be used within <OrderConfirmation>.\")\n  }\n  return context\n}\n\n/* ─── root ─────────────────────────────────────────────────────────────── */\n\nexport interface OrderConfirmationProps extends Omit<React.HTMLAttributes<HTMLElement>, \"title\"> {\n  order?: OrderConfirmationData\n  currency?: string\n  /** `undefined` follows the visitor's locale, once hydrated. */\n  locale?: string\n  /** \"Thank you for your order\", by default — \"Thank you, Ada.\" reads warmer. */\n  title?: React.ReactNode\n  /** A link that shows this order again from any device. Adds \"Copy link\", and prints. */\n  permalink?: string\n  /** What next: \"Continue shopping\", \"Track your order\". Hidden in print. */\n  actions?: React.ReactNode\n  /** The order is still loading — from the permalink, say. */\n  loading?: boolean\n  children?: React.ReactNode\n}\n\nexport function OrderConfirmation({\n  order,\n  currency = \"USD\",\n  locale: localeProp,\n  title = \"Thank you for your order\",\n  permalink,\n  actions,\n  loading = false,\n  className,\n  children,\n  ...props\n}: OrderConfirmationProps) {\n  const hydrated = useHydrated()\n  const locale = localeProp ?? (hydrated ? undefined : \"en-US\")\n  const timeZone = hydrated ? undefined : \"UTC\"\n\n  const money = React.useMemo(() => new Intl.NumberFormat(locale, { style: \"currency\", currency }), [locale, currency])\n  const day = React.useMemo(\n    () => new Intl.DateTimeFormat(locale, { weekday: \"short\", month: \"short\", day: \"numeric\", timeZone: \"UTC\" }),\n    [locale]\n  )\n  const placed = React.useMemo(\n    () => new Intl.DateTimeFormat(locale, { dateStyle: \"medium\", timeStyle: \"short\", timeZone }),\n    [locale, timeZone]\n  )\n\n  const context = React.useMemo<OrderConfirmationContextValue | null>(\n    () =>\n      order\n        ? {\n            order,\n            formatMoney: (amount) => money.format(amount),\n            formatDay: (iso) => day.format(new Date(`${iso}T00:00:00Z`)),\n            formatWindow: (from, to) =>\n              !to || to === from\n                ? day.format(new Date(`${from}T00:00:00Z`))\n                : `${day.format(new Date(`${from}T00:00:00Z`))} – ${day.format(new Date(`${to}T00:00:00Z`))}`,\n            formatPlaced: (iso) => placed.format(new Date(iso)),\n            permalink,\n            title,\n            actions,\n          }\n        : null,\n    [order, money, day, placed, permalink, title, actions]\n  )\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <article\n        data-slot=\"order-confirmation\"\n        aria-busy={loading || undefined}\n        className={cn(\"flex flex-col gap-10 print:gap-6 print:bg-white print:text-black\", className)}\n        {...props}\n      >\n        {loading || !context ? (\n          <OrderConfirmationSkeleton />\n        ) : (\n          <OrderConfirmationContext.Provider value={context}>\n            {children ?? (\n              <>\n                <OrderConfirmationHeader />\n                <OrderConfirmationFacts />\n                <div className=\"grid gap-10 lg:grid-cols-[minmax(0,1fr)_minmax(0,20rem)] lg:gap-14 print:grid-cols-[minmax(0,1fr)_16rem] print:gap-8\">\n                  <OrderConfirmationItems />\n                  <OrderConfirmationDetails />\n                </div>\n                <OrderConfirmationActions />\n              </>\n            )}\n          </OrderConfirmationContext.Provider>\n        )}\n      </article>\n    </MotionConfig>\n  )\n}\n\n/* ─── parts ────────────────────────────────────────────────────────────── */\n\n/** The band: a check that draws itself, the order number, and the thank-you. */\nexport function OrderConfirmationHeader({ className, ...props }: React.HTMLAttributes<HTMLElement>) {\n  const { order, title } = useOrderConfirmation()\n  const reduce = useReducedMotion()\n  return (\n    <header\n      data-slot=\"order-confirmation-header\"\n      className={cn(\n        \"relative isolate overflow-hidden border bg-secondary/60 px-6 py-10 sm:px-10 sm:py-14\",\n        \"print:border-0 print:bg-transparent print:p-0\",\n        className\n      )}\n      {...props}\n    >\n      <span\n        aria-hidden\n        className=\"pointer-events-none absolute inset-0 -z-10 opacity-[0.12] mix-blend-multiply dark:opacity-[0.08] dark:mix-blend-screen print:hidden\"\n        style={{ backgroundImage: GRAIN }}\n      />\n      <p className=\"flex items-center gap-3 text-xs tracking-[0.18em] text-muted-foreground uppercase print:text-black\">\n        <span className=\"grid size-7 place-items-center rounded-full bg-foreground text-background print:hidden\">\n          <svg viewBox=\"0 0 24 24\" className=\"size-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.5} aria-hidden>\n            <motion.path\n              d=\"M5 12.5 10 17 19 7.5\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              initial={reduce ? false : { pathLength: 0 }}\n              animate={{ pathLength: 1 }}\n              transition={{ ...MORPH, delay: 0.15 }}\n            />\n          </svg>\n        </span>\n        <span>\n          Order confirmed · <span className=\"text-foreground tabular-nums print:text-black\">{order.number}</span>\n        </span>\n      </p>\n      <h1 className=\"mt-5 font-serif text-4xl leading-[1.05] tracking-tight text-balance sm:text-5xl print:text-3xl\">{title}</h1>\n      {order.email ? (\n        <p className=\"mt-4 max-w-prose text-sm text-muted-foreground print:text-black\">\n          A confirmation is on its way to <span className=\"text-foreground print:text-black\">{order.email}</span>.\n        </p>\n      ) : null}\n    </header>\n  )\n}\n\n/** The four things people look for, in a ruled row: number, placed, arrives, paid. */\nexport function OrderConfirmationFacts({ className, ...props }: React.HTMLAttributes<HTMLDListElement>) {\n  const { order, formatMoney, formatWindow, formatPlaced } = useOrderConfirmation()\n  const facts: { term: string; value: React.ReactNode }[] = [\n    { term: \"Order number\", value: <span className=\"tabular-nums\">{order.number}</span> },\n    {\n      term: \"Placed\",\n      // UTC on the server, the visitor's zone right after hydration; React\n      // keeps the server's text for that first pass, then the client's.\n      value: (\n        <time dateTime={order.placedAt} suppressHydrationWarning>\n          {formatPlaced(order.placedAt)}\n        </time>\n      ),\n    },\n  ]\n  if (order.delivery?.from) {\n    facts.push({\n      term: \"Arrives\",\n      value: <time dateTime={order.delivery.from}>{formatWindow(order.delivery.from, order.delivery.to)}</time>,\n    })\n  }\n  facts.push({ term: \"Paid\", value: <span className=\"tabular-nums\">{formatMoney(order.totals.total)}</span> })\n\n  return (\n    <dl\n      data-slot=\"order-confirmation-facts\"\n      className={cn(\"grid grid-cols-2 border-y sm:grid-cols-4 print:grid-cols-4\", className)}\n      {...props}\n    >\n      {facts.map((fact, i) => (\n        <div\n          key={fact.term}\n          className={cn(\"flex flex-col gap-1.5 p-4\", i % 2 === 1 && \"border-l\", i >= 2 && \"max-sm:border-t sm:border-l\")}\n        >\n          <dt className=\"text-xs tracking-[0.14em] text-muted-foreground uppercase print:text-black\">{fact.term}</dt>\n          <dd className=\"text-sm font-medium\">{fact.value}</dd>\n        </div>\n      ))}\n    </dl>\n  )\n}\n\n/** What was bought, as a ledger, and the totals under it. */\nexport function OrderConfirmationItems({ className, ...props }: React.HTMLAttributes<HTMLElement>) {\n  const { order, formatMoney } = useOrderConfirmation()\n  const headingId = React.useId()\n  const count = order.lines.reduce((n, l) => n + l.quantity, 0)\n  const rows: { label: string; value: string; strong?: boolean }[] = [\n    { label: \"Subtotal\", value: formatMoney(order.totals.subtotal) },\n  ]\n  if (order.totals.discount) rows.push({ label: \"Discount\", value: `−${formatMoney(order.totals.discount)}` })\n  rows.push({ label: \"Shipping\", value: order.totals.shipping === 0 ? \"Free\" : formatMoney(order.totals.shipping) })\n  if (order.totals.tax !== undefined) rows.push({ label: \"Tax\", value: formatMoney(order.totals.tax) })\n\n  return (\n    <section data-slot=\"order-confirmation-items\" aria-labelledby={headingId} className={className} {...props}>\n      <h2 id={headingId} className=\"text-xs tracking-[0.14em] text-muted-foreground uppercase print:text-black\">\n        {count === 1 ? \"1 item\" : `${count} items`}\n      </h2>\n      {order.lines.length === 0 ? (\n        <p className=\"mt-3 border-t py-4 text-sm text-muted-foreground\">No items on this order.</p>\n      ) : (\n        <ul className=\"mt-3 border-t\">\n          {order.lines.map((line) => (\n            <li key={line.id} className=\"flex gap-4 border-b py-4 break-inside-avoid\">\n              {line.image ? (\n                <div className=\"relative size-16 shrink-0 overflow-hidden bg-secondary print:size-12\">{line.image}</div>\n              ) : null}\n              <div className=\"flex min-w-0 flex-1 flex-col gap-0.5 text-sm\">\n                <span className=\"font-medium\">{line.name}</span>\n                {line.variant ? <span className=\"text-muted-foreground print:text-black\">{line.variant}</span> : null}\n                <span className=\"text-muted-foreground tabular-nums print:text-black\">\n                  {line.quantity} × {formatMoney(line.unitPrice)}\n                </span>\n              </div>\n              <span className=\"text-sm tabular-nums\">{formatMoney(line.quantity * line.unitPrice)}</span>\n            </li>\n          ))}\n        </ul>\n      )}\n      <dl className=\"mt-4 flex flex-col gap-2 text-sm\">\n        {rows.map((row) => (\n          <div key={row.label} className=\"flex justify-between gap-4\">\n            <dt className=\"text-muted-foreground print:text-black\">{row.label}</dt>\n            <dd className=\"tabular-nums\">{row.value}</dd>\n          </div>\n        ))}\n        <div className=\"mt-2 flex items-baseline justify-between gap-4 border-t-2 border-foreground pt-3\">\n          <dt className=\"text-xs tracking-[0.14em] uppercase\">Total paid</dt>\n          <dd className=\"text-xl font-medium tabular-nums\">{formatMoney(order.totals.total)}</dd>\n        </div>\n      </dl>\n    </section>\n  )\n}\n\n/** Where it's going, how, and how it was paid for. */\nexport function OrderConfirmationDetails({ className, ...props }: React.HTMLAttributes<HTMLDListElement>) {\n  const { order, formatWindow } = useOrderConfirmation()\n  const blocks: { term: string; lines: React.ReactNode[] }[] = []\n  if (order.shipTo?.length) blocks.push({ term: \"Ship to\", lines: order.shipTo })\n  if (order.delivery) {\n    blocks.push({\n      term: \"Delivery\",\n      lines: [\n        order.delivery.label,\n        ...(order.delivery.from ? [`Arrives ${formatWindow(order.delivery.from, order.delivery.to)}`] : []),\n      ],\n    })\n  }\n  if (order.payment) blocks.push({ term: \"Payment\", lines: [order.payment] })\n  if (blocks.length === 0) return null\n\n  return (\n    <dl data-slot=\"order-confirmation-details\" className={cn(\"flex flex-col gap-6 text-sm\", className)} {...props}>\n      {blocks.map((block) => (\n        <div key={block.term} className=\"border-t pt-3 break-inside-avoid\">\n          <dt className=\"mb-1.5 text-xs tracking-[0.14em] text-muted-foreground uppercase print:text-black\">{block.term}</dt>\n          {block.lines.map((line, i) => (\n            <dd key={i} className=\"leading-relaxed\">\n              {line}\n            </dd>\n          ))}\n        </div>\n      ))}\n    </dl>\n  )\n}\n\n/**\n * Your next actions, then print and copy-link. Hidden in print, where the\n * permalink is written out instead.\n */\nexport function OrderConfirmationActions({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n  const { actions, permalink } = useOrderConfirmation()\n  const reduce = useReducedMotion()\n  const [copied, setCopied] = React.useState<\"idle\" | \"copied\" | \"selected\">(\"idle\")\n  const timer = React.useRef(0)\n  const linkRef = React.useRef<HTMLAnchorElement>(null)\n  React.useEffect(() => () => window.clearTimeout(timer.current), [])\n\n  const copy = async () => {\n    if (!permalink) return\n    let outcome: \"copied\" | \"selected\" = \"copied\"\n    try {\n      await navigator.clipboard.writeText(permalink)\n    } catch {\n      // The browser refused the clipboard (an insecure context, a denied\n      // permission): select the link on the page instead, one keystroke from copied.\n      const link = linkRef.current\n      if (link) window.getSelection()?.selectAllChildren(link)\n      outcome = \"selected\"\n    }\n    setCopied(outcome)\n    window.clearTimeout(timer.current)\n    timer.current = window.setTimeout(() => setCopied(\"idle\"), outcome === \"copied\" ? 2000 : 4000)\n  }\n\n  return (\n    <div data-slot=\"order-confirmation-actions\" className={cn(\"flex flex-col gap-4\", className)} {...props}>\n      <div className=\"flex flex-wrap items-center gap-3 print:hidden\">\n        {actions}\n        <Button type=\"button\" variant=\"outline\" onClick={() => window.print()}>\n          <Printer aria-hidden />\n          Print receipt\n        </Button>\n        {permalink ? (\n          <Button type=\"button\" variant=\"outline\" onClick={copy} aria-live=\"polite\">\n            {copied === \"copied\" ? <Check aria-hidden /> : <Link2 aria-hidden />}\n            <span className=\"relative inline-grid\">\n              {/* Both labels hold the width; the one on show crossfades with a lift. */}\n              <span aria-hidden className=\"invisible col-start-1 row-start-1\">\n                Copy order link\n              </span>\n              <AnimatePresence initial={false} mode=\"popLayout\">\n                <motion.span\n                  key={copied}\n                  className=\"col-start-1 row-start-1\"\n                  initial={reduce ? { opacity: 0 } : { opacity: 0, y: 6 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={reduce ? { opacity: 0 } : { opacity: 0, y: -6 }}\n                  transition={reduce ? { duration: 0.15 } : MORPH}\n                >\n                  {copied === \"copied\" ? \"Link copied\" : copied === \"selected\" ? \"Link selected\" : \"Copy order link\"}\n                </motion.span>\n              </AnimatePresence>\n            </span>\n          </Button>\n        ) : null}\n      </div>\n      {permalink ? (\n        <p className=\"text-xs break-all text-muted-foreground print:text-black\">\n          <span className=\"print:hidden\">Come back to this order any time: </span>\n          <span className=\"hidden print:inline\">This order online: </span>\n          <a ref={linkRef} href={permalink} className=\"underline underline-offset-4\">\n            {permalink}\n          </a>\n        </p>\n      ) : null}\n    </div>\n  )\n}\n\n/** The receipt's shape while the order loads. */\nfunction OrderConfirmationSkeleton() {\n  return (\n    <div data-slot=\"order-confirmation-skeleton\" className=\"flex flex-col gap-10\">\n      <span className=\"sr-only\" role=\"status\">\n        Loading your order\n      </span>\n      <div aria-hidden className=\"flex flex-col gap-5 border px-6 py-10 sm:px-10 sm:py-14\">\n        <Skeleton className=\"h-3 w-48 rounded-none\" />\n        <Skeleton className=\"h-10 w-3/4 max-w-md rounded-none\" />\n        <Skeleton className=\"h-3.5 w-64 rounded-none\" />\n      </div>\n      <div aria-hidden className=\"grid grid-cols-2 gap-4 border-y py-4 sm:grid-cols-4\">\n        {[0, 1, 2, 3].map((i) => (\n          <div key={i} className=\"flex flex-col gap-2\">\n            <Skeleton className=\"h-3 w-20 rounded-none\" />\n            <Skeleton className=\"h-4 w-28 rounded-none\" />\n          </div>\n        ))}\n      </div>\n      <div aria-hidden className=\"flex flex-col gap-4\">\n        {[0, 1].map((i) => (\n          <div key={i} className=\"flex gap-4\">\n            <Skeleton className=\"size-16 rounded-none\" />\n            <div className=\"flex flex-1 flex-col gap-2\">\n              <Skeleton className=\"h-3.5 w-40 rounded-none\" />\n              <Skeleton className=\"h-3 w-24 rounded-none\" />\n            </div>\n          </div>\n        ))}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ecomcn/order-confirmation.tsx"
    }
  ],
  "categories": [
    "ecommerce",
    "post-purchase"
  ],
  "type": "registry:block"
}