{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "checkout-summary",
  "type": "registry:component",
  "title": "Checkout summary",
  "description": "Order summary for inline checkout: line items, totals, recurring/trial info, and refund policy link. Compliance-ready display for Paddle inline checkout.",
  "dependencies": [],
  "registryDependencies": [
    "@paddle/paddle-helpers",
    "card",
    "separator",
    "skeleton"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/checkout-summary/components/checkout-summary.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/registry/new-york/ui/card\"\nimport { Separator } from \"@/registry/new-york/ui/separator\"\nimport { Skeleton } from \"@/registry/new-york/ui/skeleton\"\nimport { cn } from \"@/lib/utils\"\nimport type { CheckoutSummaryData } from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-types\"\nimport {\n  formatMoney,\n  formatBillingCycle,\n} from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-format\"\n\n/** Props for the `CheckoutSummary` component. */\nexport type CheckoutSummaryProps = {\n  summary?: CheckoutSummaryData\n  policyUrl?: string\n  policyLabel?: string\n  className?: string\n}\n\nexport function CheckoutSummary({\n  summary,\n  policyUrl,\n  policyLabel = \"Refund policy\",\n  className,\n}: CheckoutSummaryProps) {\n  if (!summary) {\n    return <CheckoutSummarySkeleton className={className} />\n  }\n\n  const {\n    items,\n    subtotal,\n    tax,\n    total,\n    discount,\n    currency,\n    recurringTotal,\n    recurringInterval,\n    recurringFrequency,\n    trialPeriod,\n  } = summary\n\n  const recurringInfo =\n    recurringTotal !== undefined && recurringInterval\n      ? (() => {\n          const cycleLabel = formatBillingCycle({\n            frequency: recurringFrequency ?? 1,\n            interval: recurringInterval,\n          })\n          const recurringAmount = formatMoney(recurringTotal, currency)\n          return cycleLabel ? `${recurringAmount} / ${cycleLabel}` : recurringAmount\n        })()\n      : undefined\n\n  return (\n    <Card className={cn(\"gap-4\", className)}>\n      <CardHeader>\n        <CardTitle className=\"text-base font-semibold\">Order summary</CardTitle>\n      </CardHeader>\n\n      <CardContent className=\"flex flex-col gap-4\">\n        <ul className=\"space-y-3\">\n          {items.map((item, index) => (\n            <li key={index} className=\"flex items-start justify-between gap-4 text-sm\">\n              <div className=\"min-w-0 flex-1\">\n                <span className=\"font-medium\">{item.name}</span>\n                {item.priceName && (\n                  <span className=\"text-muted-foreground\"> — {item.priceName}</span>\n                )}\n                {item.quantity > 1 && (\n                  <span className=\"text-muted-foreground\"> × {item.quantity}</span>\n                )}\n              </div>\n              <span className=\"shrink-0 tabular-nums\">{formatMoney(item.lineTotal, currency)}</span>\n            </li>\n          ))}\n        </ul>\n\n        <Separator />\n\n        <dl className=\"space-y-2 text-sm\">\n          <div className=\"flex justify-between\">\n            <dt className=\"text-muted-foreground\">Subtotal</dt>\n            <dd className=\"tabular-nums\">{formatMoney(subtotal, currency)}</dd>\n          </div>\n\n          {discount != null && discount > 0 && (\n            <div className=\"flex justify-between text-success-foreground\">\n              <dt>Discount</dt>\n              <dd className=\"tabular-nums\">−{formatMoney(discount, currency)}</dd>\n            </div>\n          )}\n\n          <div className=\"flex justify-between\">\n            <dt className=\"text-muted-foreground\">Tax</dt>\n            <dd className=\"tabular-nums\">{formatMoney(tax, currency)}</dd>\n          </div>\n\n          <Separator />\n\n          <div className=\"flex justify-between font-semibold\">\n            <dt>Total</dt>\n            <dd className=\"tabular-nums\">{formatMoney(total, currency)}</dd>\n          </div>\n        </dl>\n\n        {(recurringInfo || trialPeriod) && (\n          <p className=\"text-muted-foreground text-xs\">\n            {trialPeriod && <span>{trialPeriod} free trial. </span>}\n            {recurringInfo && <span>Then {recurringInfo}.</span>}\n          </p>\n        )}\n\n        {policyUrl && (\n          <a\n            href={policyUrl}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"text-muted-foreground hover:text-foreground text-xs underline transition-colors\"\n          >\n            {policyLabel}\n          </a>\n        )}\n      </CardContent>\n    </Card>\n  )\n}\n\nfunction CheckoutSummarySkeleton({ className }: { className?: string }) {\n  return (\n    <Card className={cn(\"gap-4\", className)}>\n      <CardHeader>\n        <Skeleton className=\"h-4 w-28\" />\n      </CardHeader>\n      <CardContent className=\"flex flex-col gap-4\">\n        <div className=\"space-y-3\">\n          <div className=\"flex justify-between gap-4\">\n            <Skeleton className=\"h-4 w-36\" />\n            <Skeleton className=\"h-4 w-16\" />\n          </div>\n          <div className=\"flex justify-between gap-4\">\n            <Skeleton className=\"h-4 w-28\" />\n            <Skeleton className=\"h-4 w-16\" />\n          </div>\n        </div>\n        <Separator />\n        <div className=\"space-y-2\">\n          <div className=\"flex justify-between gap-4\">\n            <Skeleton className=\"h-4 w-16\" />\n            <Skeleton className=\"h-4 w-14\" />\n          </div>\n          <div className=\"flex justify-between gap-4\">\n            <Skeleton className=\"h-4 w-8\" />\n            <Skeleton className=\"h-4 w-14\" />\n          </div>\n          <Separator />\n          <div className=\"flex justify-between gap-4\">\n            <Skeleton className=\"h-4 w-10\" />\n            <Skeleton className=\"h-4 w-16\" />\n          </div>\n        </div>\n        <Skeleton className=\"h-3 w-48\" />\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/new-york/blocks/checkout-summary/components/paddle-checkout-summary.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { mapCheckoutEventsToSummary } from \"@/registry/new-york/blocks/checkout-summary/lib/checkout-summary-utils\"\nimport { CheckoutSummary, type CheckoutSummaryProps } from \"./checkout-summary\"\n\ntype CheckoutEventsInput = Parameters<typeof mapCheckoutEventsToSummary>[0]\n\nexport type PaddleCheckoutSummaryProps = {\n  /**\n   * Raw `CheckoutEventsData` from a Paddle.js checkout event callback\n   * (e.g. `checkout.loaded`, `checkout.updated`).\n   */\n  checkoutData?: CheckoutEventsInput\n} & Omit<CheckoutSummaryProps, \"summary\">\n\n/**\n * Paddle-aware wrapper for `CheckoutSummary`.\n *\n * Accepts raw `CheckoutEventsData` from Paddle.js checkout event callbacks,\n * maps it to `CheckoutSummaryData`, and renders the order summary UI.\n *\n * Pass `undefined` to render the skeleton loading state.\n *\n * @example\n * const [checkoutData, setCheckoutData] = useState<CheckoutEventsData>()\n *\n * <PaddleCheckoutSummary\n *   checkoutData={checkoutData}\n *   policyUrl=\"/legal/refunds\"\n * />\n */\nexport function PaddleCheckoutSummary({ checkoutData, ...uiProps }: PaddleCheckoutSummaryProps) {\n  const summary = checkoutData ? mapCheckoutEventsToSummary(checkoutData) : undefined\n  return <CheckoutSummary summary={summary} {...uiProps} />\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/new-york/blocks/checkout-summary/lib/checkout-summary-utils.ts",
      "content": "import type { CheckoutSummaryData } from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-types\"\nimport { formatTrialPeriod } from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-format\"\n\n// ---\n// Input shapes — minimal types matching the Paddle.js CheckoutEventsData shape.\n// Field names use snake_case (Paddle.js convention). Pass the raw checkout event\n// payload or any object matching this shape.\n// ---\n\ntype TimePeriodLike = {\n  interval: string\n  frequency: number\n}\n\ntype CheckoutEventItem = {\n  product?: { name?: string | null } | null\n  price_name?: string | null\n  quantity?: number | null\n  totals?: { subtotal?: number | null } | null\n  billing_cycle?: TimePeriodLike | null\n  trial_period?: TimePeriodLike | null\n}\n\ntype CheckoutEventsInput = {\n  currency_code?: string | null\n  items?: CheckoutEventItem[] | null\n  totals?: {\n    subtotal?: number | null\n    tax?: number | null\n    total?: number | null\n    discount?: number | null\n  } | null\n  recurring_totals?: {\n    total?: number | null\n  } | null\n}\n\n/**\n * Maps a Paddle.js checkout event payload to the `CheckoutSummaryData`\n * display contract consumed by `CheckoutSummary`.\n *\n * Paddle.js checkout event totals are already decimal numbers (not lowest-denomination),\n * so no cents-to-decimal conversion is needed here. For Paddle API amounts, use\n * `parseAmount()` from `paddle-format` — those are returned in lowest denomination.\n *\n * @param data - Paddle.js checkout event data (e.g. from `checkout.loaded` / `checkout.updated`)\n * @returns Mapped summary data for `<CheckoutSummary />`\n *\n * @example\n * paddle.Update({ eventCallback: (event) => {\n *   const summary = mapCheckoutEventsToSummary(event.data)\n * }})\n */\nexport function mapCheckoutEventsToSummary(data: CheckoutEventsInput): CheckoutSummaryData {\n  const currency = data.currency_code ?? \"\"\n\n  const items = (data.items ?? []).map((item) => ({\n    name: item.product?.name ?? \"\",\n    priceName: item.price_name ?? undefined,\n    quantity: item.quantity ?? 1,\n    lineTotal: item.totals?.subtotal ?? 0,\n  }))\n\n  const totals = data.totals\n  const recurringTotals = data.recurring_totals\n\n  const discount = totals?.discount && totals.discount > 0 ? totals.discount : undefined\n\n  // Build recurring billing info from the first item that has a billing cycle.\n  // Using find() rather than [0] handles mixed carts (e.g. one-time setup fee\n  // followed by a recurring subscription) where the first item may not recur.\n  let recurringTotal: number | undefined\n  let recurringInterval: string | undefined\n  let recurringFrequency: number | undefined\n  const firstRecurringItem = data.items?.find((item) => item.billing_cycle != null)\n  const billingCycle = firstRecurringItem?.billing_cycle\n  if (billingCycle && recurringTotals?.total != null) {\n    recurringTotal = recurringTotals.total\n    recurringInterval = billingCycle.interval\n    recurringFrequency = billingCycle.frequency\n  }\n\n  // Build trial period label from the first item that has a trial period.\n  let trialPeriod: string | undefined\n  const firstTrialItem = data.items?.find((item) => item.trial_period != null)\n  if (firstTrialItem?.trial_period) {\n    trialPeriod = formatTrialPeriod(firstTrialItem.trial_period)\n  }\n\n  return {\n    items,\n    subtotal: totals?.subtotal ?? 0,\n    tax: totals?.tax ?? 0,\n    total: totals?.total ?? 0,\n    discount,\n    currency,\n    recurringTotal,\n    recurringInterval,\n    recurringFrequency,\n    trialPeriod,\n  }\n}\n",
      "type": "registry:lib"
    }
  ],
  "categories": ["checkout"]
}
