{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "plan-change-breakdown",
  "type": "registry:component",
  "title": "Plan change breakdown",
  "description": "Detailed financial breakdown of a subscription plan change. Invoice-style view with per-transaction line items, proration periods, tax, credits, and totals for immediate, next, and recurring billing. Financial-transparency view complementing the compact plan-change-preview.",
  "dependencies": ["lucide-react"],
  "registryDependencies": [
    "@paddle/paddle-helpers",
    "card",
    "badge",
    "skeleton",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/plan-change-breakdown/components/plan-change-breakdown.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { TrendingDown, TrendingUp, Minus } from \"lucide-react\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/registry/new-york/ui/card\"\nimport { Badge } from \"@/registry/new-york/ui/badge\"\nimport { Skeleton } from \"@/registry/new-york/ui/skeleton\"\nimport { Separator } from \"@/registry/new-york/ui/separator\"\nimport { cn } from \"@/lib/utils\"\nimport type {\n  PlanChangeBreakdownData,\n  PlanChangeTransactionSectionData,\n} from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-types\"\nimport {\n  formatMoney,\n  formatDate,\n} from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-format\"\n\n/** Props for the `PlanChangeBreakdown` component. */\nexport type PlanChangeBreakdownProps = {\n  breakdown?: PlanChangeBreakdownData\n  /**\n   * Payment collection mode. From `subscription.collection_mode`.\n   * Affects section titles: \"Charged Today\" vs \"Invoice Created\" for immediate transactions.\n   */\n  collectionMode?: \"automatic\" | \"manual\"\n  className?: string\n}\n\nconst SECTION_TITLES: Record<\n  \"immediate\" | \"next\" | \"recurring\",\n  { automatic: string; manual: string }\n> = {\n  immediate: { automatic: \"Charged today\", manual: \"Invoice created\" },\n  next: { automatic: \"Next invoice\", manual: \"Next invoice\" },\n  recurring: { automatic: \"Ongoing billing\", manual: \"Ongoing billing\" },\n}\n\nfunction TransactionSection({\n  section,\n  kind,\n  collectionMode = \"automatic\",\n  currency,\n}: {\n  section: PlanChangeTransactionSectionData\n  kind: \"immediate\" | \"next\" | \"recurring\"\n  collectionMode?: \"automatic\" | \"manual\"\n  currency: string\n}) {\n  const title = SECTION_TITLES[kind][collectionMode]\n\n  const description =\n    kind === \"immediate\"\n      ? collectionMode === \"manual\"\n        ? \"An invoice will be created for this amount\"\n        : \"This amount will be charged immediately\"\n      : kind === \"next\"\n        ? section.billingDate\n          ? `Charged on ${formatDate(section.billingDate)}`\n          : undefined\n        : \"Recurring amount after this change\"\n\n  return (\n    <div className=\"flex flex-col gap-3\">\n      <div>\n        <h4 className=\"text-sm font-medium\">{title}</h4>\n        {description && <p className=\"text-xs text-muted-foreground\">{description}</p>}\n      </div>\n\n      <div className=\"flex flex-col gap-2\">\n        {section.lineItems.map((item, index) => (\n          <div key={index} className=\"flex items-start justify-between gap-4 text-sm\">\n            <div className=\"flex flex-col gap-0.5 min-w-0\">\n              <span className=\"font-medium truncate\">{item.productName}</span>\n              <span className=\"text-xs text-muted-foreground\">\n                {item.quantity > 1 && `${item.quantity} \\u00d7 `}\n                {formatMoney(item.unitPrice, currency)}\n                {item.isProrated && (\n                  <Badge variant=\"secondary\" className=\"ml-1 text-[10px] px-1 py-0\">\n                    Prorated\n                  </Badge>\n                )}\n              </span>\n              {item.prorationPeriod && (\n                <span className=\"text-xs text-muted-foreground\">{item.prorationPeriod}</span>\n              )}\n            </div>\n            <span className=\"tabular-nums shrink-0\">{formatMoney(item.total, currency)}</span>\n          </div>\n        ))}\n      </div>\n\n      <Separator />\n\n      <dl className=\"flex flex-col gap-1.5 text-sm\">\n        <div className=\"flex justify-between gap-4\">\n          <dt className=\"text-muted-foreground\">Subtotal</dt>\n          <dd className=\"tabular-nums\">{formatMoney(section.totals.subtotal, currency)}</dd>\n        </div>\n        {section.totals.discount !== undefined && (\n          <div className=\"flex justify-between gap-4\">\n            <dt className=\"text-success-foreground\">Discount</dt>\n            <dd className=\"tabular-nums text-success-foreground\">\n              −{formatMoney(section.totals.discount, currency)}\n            </dd>\n          </div>\n        )}\n        <div className=\"flex justify-between gap-4\">\n          <dt className=\"text-muted-foreground\">Tax</dt>\n          <dd className=\"tabular-nums\">{formatMoney(section.totals.tax, currency)}</dd>\n        </div>\n        {section.totals.credit !== undefined && (\n          <div className=\"flex justify-between gap-4\">\n            <dt className=\"text-success-foreground\">Credit applied</dt>\n            <dd className=\"tabular-nums text-success-foreground\">\n              −{formatMoney(section.totals.credit, currency)}\n            </dd>\n          </div>\n        )}\n        {section.totals.creditToBalance !== undefined && (\n          <div className=\"flex justify-between gap-4\">\n            <dt className=\"text-success-foreground\">Credit to balance</dt>\n            <dd className=\"tabular-nums text-success-foreground\">\n              {formatMoney(section.totals.creditToBalance, currency)}\n            </dd>\n          </div>\n        )}\n        <div className=\"flex justify-between gap-4 pt-1.5 border-t font-medium\">\n          <dt>Total</dt>\n          <dd className=\"tabular-nums\">{formatMoney(section.totals.total, currency)}</dd>\n        </div>\n      </dl>\n    </div>\n  )\n}\n\nexport function PlanChangeBreakdown({\n  breakdown,\n  collectionMode,\n  className,\n}: PlanChangeBreakdownProps) {\n  if (!breakdown) {\n    return <PlanChangeBreakdownSkeleton className={className} />\n  }\n\n  const {\n    currency,\n    result,\n    breakdown: summaryBreakdown,\n    immediateTransaction,\n    nextTransaction,\n    recurringTransaction,\n  } = breakdown\n  const isCredit = result.direction === \"credit\"\n  const isNone = result.direction === \"none\"\n\n  const resultLabel = isNone\n    ? \"No charge\"\n    : isCredit\n      ? \"Credit to account\"\n      : immediateTransaction\n        ? \"Amount due\"\n        : \"Added to next bill\"\n\n  return (\n    <Card className={cn(\"flex flex-col\", className)}>\n      <CardHeader>\n        <CardTitle className=\"text-base font-semibold\">Change summary</CardTitle>\n        <CardDescription>Review the financial impact of this change</CardDescription>\n      </CardHeader>\n      <CardContent className=\"flex flex-col gap-6\">\n        <div\n          className={cn(\n            \"flex items-center justify-between rounded-lg p-4\",\n            isCredit ? \"bg-success/10\" : \"bg-muted\"\n          )}\n        >\n          <div className=\"flex items-center gap-2\">\n            {isCredit ? (\n              <TrendingDown className=\"size-5 text-success-foreground\" />\n            ) : isNone ? (\n              <Minus className=\"size-5 text-muted-foreground\" />\n            ) : (\n              <TrendingUp className=\"size-5 text-foreground\" />\n            )}\n            <span className=\"font-medium\">{resultLabel}</span>\n          </div>\n          <span\n            className={cn(\"text-lg font-bold tabular-nums\", isCredit && \"text-success-foreground\")}\n          >\n            {isCredit\n              ? `−${formatMoney(result.amount, currency)}`\n              : formatMoney(result.amount, currency)}\n          </span>\n        </div>\n\n        {/* Credit/charge breakdown from update_summary */}\n        {summaryBreakdown &&\n          (summaryBreakdown.credit !== undefined || summaryBreakdown.charge !== undefined) && (\n            <div className=\"flex flex-col gap-2 text-sm\">\n              {summaryBreakdown.credit !== undefined && (\n                <div className=\"flex items-center justify-between gap-4\">\n                  <span className=\"text-muted-foreground\">Credit from current plan</span>\n                  <span className=\"text-success-foreground tabular-nums\">\n                    −{formatMoney(summaryBreakdown.credit, currency)}\n                  </span>\n                </div>\n              )}\n              {summaryBreakdown.charge !== undefined && (\n                <div className=\"flex items-center justify-between gap-4\">\n                  <span className=\"text-muted-foreground\">Charge for new plan</span>\n                  <span className=\"tabular-nums\">\n                    {formatMoney(summaryBreakdown.charge, currency)}\n                  </span>\n                </div>\n              )}\n            </div>\n          )}\n\n        {immediateTransaction && (\n          <>\n            <Separator />\n            <TransactionSection\n              section={immediateTransaction}\n              kind=\"immediate\"\n              collectionMode={collectionMode}\n              currency={currency}\n            />\n          </>\n        )}\n\n        {nextTransaction && (\n          <>\n            <Separator />\n            <TransactionSection\n              section={nextTransaction}\n              kind=\"next\"\n              collectionMode={collectionMode}\n              currency={currency}\n            />\n          </>\n        )}\n\n        {recurringTransaction && (\n          <>\n            <Separator />\n            <TransactionSection\n              section={recurringTransaction}\n              kind=\"recurring\"\n              collectionMode={collectionMode}\n              currency={currency}\n            />\n          </>\n        )}\n      </CardContent>\n    </Card>\n  )\n}\n\nfunction PlanChangeBreakdownSkeleton({ className }: { className?: string }) {\n  return (\n    <Card className={cn(\"flex flex-col\", className)}>\n      <CardHeader>\n        <Skeleton className=\"h-4 w-32\" />\n        <Skeleton className=\"h-3 w-56\" />\n      </CardHeader>\n      <CardContent className=\"flex flex-col gap-6\">\n        <div className=\"rounded-lg bg-muted p-4 flex items-center justify-between\">\n          <div className=\"flex items-center gap-2\">\n            <Skeleton className=\"h-5 w-5 rounded\" />\n            <Skeleton className=\"h-4 w-28\" />\n          </div>\n          <Skeleton className=\"h-6 w-16\" />\n        </div>\n\n        <div className=\"space-y-2\">\n          <div className=\"flex justify-between gap-4\">\n            <Skeleton className=\"h-3 w-36\" />\n            <Skeleton className=\"h-3 w-16\" />\n          </div>\n          <div className=\"flex justify-between gap-4\">\n            <Skeleton className=\"h-3 w-32\" />\n            <Skeleton className=\"h-3 w-16\" />\n          </div>\n        </div>\n\n        <Separator />\n\n        <div className=\"space-y-3\">\n          <div className=\"space-y-1\">\n            <Skeleton className=\"h-4 w-24\" />\n            <Skeleton className=\"h-3 w-48\" />\n          </div>\n          <div className=\"space-y-2\">\n            <div className=\"flex justify-between gap-4\">\n              <div className=\"space-y-1\">\n                <Skeleton className=\"h-4 w-20\" />\n                <Skeleton className=\"h-3 w-16\" />\n              </div>\n              <Skeleton className=\"h-4 w-14\" />\n            </div>\n          </div>\n          <Separator />\n          <div className=\"space-y-1.5\">\n            <div className=\"flex justify-between gap-4\">\n              <Skeleton className=\"h-3 w-16\" />\n              <Skeleton className=\"h-3 w-14\" />\n            </div>\n            <div className=\"flex justify-between gap-4\">\n              <Skeleton className=\"h-3 w-8\" />\n              <Skeleton className=\"h-3 w-10\" />\n            </div>\n            <div className=\"flex justify-between gap-4 pt-1.5 border-t\">\n              <Skeleton className=\"h-4 w-12\" />\n              <Skeleton className=\"h-4 w-14\" />\n            </div>\n          </div>\n        </div>\n\n        <Separator />\n\n        <div className=\"space-y-3\">\n          <div className=\"space-y-1\">\n            <Skeleton className=\"h-4 w-28\" />\n            <Skeleton className=\"h-3 w-52\" />\n          </div>\n          <div className=\"space-y-2\">\n            <div className=\"flex justify-between gap-4\">\n              <div className=\"space-y-1\">\n                <Skeleton className=\"h-4 w-20\" />\n                <Skeleton className=\"h-3 w-16\" />\n              </div>\n              <Skeleton className=\"h-4 w-14\" />\n            </div>\n          </div>\n          <Separator />\n          <div className=\"space-y-1.5\">\n            <div className=\"flex justify-between gap-4\">\n              <Skeleton className=\"h-3 w-16\" />\n              <Skeleton className=\"h-3 w-14\" />\n            </div>\n            <div className=\"flex justify-between gap-4\">\n              <Skeleton className=\"h-3 w-8\" />\n              <Skeleton className=\"h-3 w-10\" />\n            </div>\n            <div className=\"flex justify-between gap-4 pt-1.5 border-t\">\n              <Skeleton className=\"h-4 w-12\" />\n              <Skeleton className=\"h-4 w-14\" />\n            </div>\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/new-york/blocks/plan-change-breakdown/components/paddle-plan-change-breakdown.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { mapPreviewToBreakdownData } from \"@/registry/new-york/blocks/plan-change-breakdown/lib/plan-change-breakdown-utils\"\nimport { PlanChangeBreakdown, type PlanChangeBreakdownProps } from \"./plan-change-breakdown\"\n\ntype BreakdownPreviewResponse = Parameters<typeof mapPreviewToBreakdownData>[0]\n\nexport type PaddlePlanChangeBreakdownProps = {\n  /** Subscription update preview response with the proposed changes */\n  previewResponse: BreakdownPreviewResponse\n} & Omit<PlanChangeBreakdownProps, \"breakdown\">\n\n/**\n * Paddle-aware wrapper for `PlanChangeBreakdown`.\n *\n * Accepts the response from the Paddle preview update endpoint, maps it to\n * `PlanChangeBreakdownData`, and renders the detailed financial breakdown UI.\n *\n * Pass `collectionMode` from the subscription to control billing terminology\n * (e.g. \"Invoice created\" vs \"Charged today\").\n *\n * @example\n * <PaddlePlanChangeBreakdown\n *   previewResponse={previewResponse}\n *   prorationBillingMode=\"prorated_immediately\"\n *   collectionMode={subscription.collectionMode}\n * />\n */\nexport function PaddlePlanChangeBreakdown({\n  previewResponse,\n  ...uiProps\n}: PaddlePlanChangeBreakdownProps) {\n  const breakdown = mapPreviewToBreakdownData(previewResponse)\n  return <PlanChangeBreakdown breakdown={breakdown} {...uiProps} />\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/new-york/blocks/plan-change-breakdown/lib/plan-change-breakdown-utils.ts",
      "content": "import type {\n  PlanChangeBreakdownData,\n  PlanChangeLineItemData,\n  PlanChangeTransactionSectionData,\n  PlanChangeTransactionTotalsData,\n} from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-types\"\nimport { parseAmount } from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-format\"\n\n// ---\n// Input shapes — matching Paddle Node SDK / API response structures.\n// `immediate_transaction` and `next_transaction` share a shape with\n// `details.line_items` and `details.totals`. `recurring_transaction_details`\n// has `line_items` and `totals` at the top level (no `details` wrapper)\n// and omits `billing_period`.\n// ---\n\ntype AmountWithCurrency = {\n  amount: string\n  currencyCode: string\n}\n\ntype LineItemTotals = {\n  subtotal: string\n  total: string\n  tax: string\n  discount: string\n}\n\ntype Proration = {\n  rate: string\n  billingPeriod: {\n    startsAt: string\n    endsAt: string\n  }\n} | null\n\ntype LineItem = {\n  product: { name: string }\n  quantity: number\n  unitTotals: { subtotal: string }\n  totals: LineItemTotals\n  proration?: Proration\n}\n\ntype TransactionTotals = {\n  subtotal: string\n  discount: string\n  tax: string\n  total: string\n  grandTotal: string\n  credit: string\n  creditToBalance?: string\n}\n\ntype TransactionPreview = {\n  billingPeriod?: { startsAt?: string | null; endsAt?: string | null } | null\n  details: {\n    totals: TransactionTotals\n    lineItems: LineItem[]\n  }\n}\n\ntype RecurringTransactionDetails = {\n  totals: TransactionTotals & { currencyCode: string }\n  lineItems: LineItem[]\n}\n\ntype UpdateSummary = {\n  credit: AmountWithCurrency\n  charge: AmountWithCurrency\n  result: {\n    action: \"credit\" | \"charge\"\n    amount: string\n    currencyCode: string\n  }\n}\n\ntype BreakdownPreviewResponse = {\n  currencyCode: string\n  updateSummary: UpdateSummary\n  immediateTransaction?: TransactionPreview | null\n  nextTransaction?: TransactionPreview | null\n  recurringTransactionDetails?: RecurringTransactionDetails | null\n}\n\nfunction mapLineItems(items: LineItem[], currencyCode: string): PlanChangeLineItemData[] {\n  return items.map((item) => {\n    const hasProration = item.proration != null\n    let prorationPeriod: string | undefined\n\n    if (hasProration && item.proration) {\n      const rate = parseFloat(item.proration.rate)\n      if (!isNaN(rate) && rate > 0 && rate < 1) {\n        const start = new Date(item.proration.billingPeriod.startsAt)\n        const end = new Date(item.proration.billingPeriod.endsAt)\n        const totalDays = Math.round((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24))\n        const proratedDays = Math.round(totalDays * rate)\n        prorationPeriod = `${proratedDays} of ${totalDays} days`\n      }\n    }\n\n    return {\n      productName: item.product.name,\n      quantity: item.quantity,\n      unitPrice: parseAmount(item.unitTotals.subtotal, currencyCode),\n      total: parseAmount(item.totals.total, currencyCode),\n      isProrated: hasProration || undefined,\n      prorationPeriod,\n    }\n  })\n}\n\nfunction mapTotals(\n  totals: TransactionTotals,\n  currencyCode: string\n): PlanChangeTransactionTotalsData {\n  const discount = parseAmount(totals.discount, currencyCode)\n  const credit = parseAmount(totals.credit, currencyCode)\n  const creditToBalance = totals.creditToBalance\n    ? parseAmount(totals.creditToBalance, currencyCode)\n    : 0\n\n  return {\n    subtotal: parseAmount(totals.subtotal, currencyCode),\n    discount: discount > 0 ? discount : undefined,\n    tax: parseAmount(totals.tax, currencyCode),\n    credit: credit > 0 ? credit : undefined,\n    creditToBalance: creditToBalance > 0 ? creditToBalance : undefined,\n    total: parseAmount(totals.grandTotal, currencyCode),\n  }\n}\n\n/**\n * Maps a Paddle subscription preview API response to the `PlanChangeBreakdownData`\n * display contract consumed by `PlanChangeBreakdown`.\n *\n * Provides full financial detail — line items, tax, proration periods, and totals.\n * Pass `collectionMode` directly to `<PlanChangeBreakdown>` as a prop.\n *\n * @param preview - Subscription update preview response\n * @returns Mapped breakdown data for `<PlanChangeBreakdown />`\n *\n * @example\n * const preview = await paddle.subscriptions.previewUpdate(subscriptionId, {\n *   items: [{ priceId: newPriceId, quantity: 1 }],\n *   prorationBillingMode: \"prorated_immediately\",\n * })\n * const data = mapPreviewToBreakdownData(preview)\n */\nexport function mapPreviewToBreakdownData(\n  preview: BreakdownPreviewResponse\n): PlanChangeBreakdownData {\n  const {\n    currencyCode,\n    updateSummary,\n    immediateTransaction,\n    nextTransaction,\n    recurringTransactionDetails,\n  } = preview\n\n  const resultAmount = parseAmount(updateSummary.result.amount, currencyCode)\n  const creditAmount = parseAmount(updateSummary.credit.amount, currencyCode)\n  const chargeAmount = parseAmount(updateSummary.charge.amount, currencyCode)\n  const isNone = resultAmount === 0\n\n  const result: PlanChangeBreakdownData[\"result\"] = {\n    direction: isNone ? \"none\" : updateSummary.result.action,\n    amount: resultAmount,\n  }\n\n  const breakdown: PlanChangeBreakdownData[\"breakdown\"] =\n    creditAmount > 0 || chargeAmount > 0\n      ? {\n          credit: creditAmount > 0 ? creditAmount : undefined,\n          charge: chargeAmount > 0 ? chargeAmount : undefined,\n        }\n      : undefined\n\n  let immediateSectionData: PlanChangeTransactionSectionData | undefined\n  if (immediateTransaction) {\n    immediateSectionData = {\n      lineItems: mapLineItems(immediateTransaction.details.lineItems, currencyCode),\n      totals: mapTotals(immediateTransaction.details.totals, currencyCode),\n    }\n  }\n\n  let nextSectionData: PlanChangeTransactionSectionData | undefined\n  if (nextTransaction) {\n    nextSectionData = {\n      billingDate: nextTransaction.billingPeriod?.startsAt ?? undefined,\n      lineItems: mapLineItems(nextTransaction.details.lineItems, currencyCode),\n      totals: mapTotals(nextTransaction.details.totals, currencyCode),\n    }\n  }\n\n  let recurringSectionData: PlanChangeTransactionSectionData | undefined\n  if (recurringTransactionDetails) {\n    recurringSectionData = {\n      lineItems: mapLineItems(recurringTransactionDetails.lineItems, currencyCode),\n      totals: mapTotals(recurringTransactionDetails.totals, currencyCode),\n    }\n  }\n\n  return {\n    currency: currencyCode,\n    result,\n    breakdown,\n    immediateTransaction: immediateSectionData,\n    nextTransaction: nextSectionData,\n    recurringTransaction: recurringSectionData,\n  }\n}\n",
      "type": "registry:lib"
    }
  ],
  "cssVars": {
    "light": {
      "success": "oklch(0.723 0.157 150)",
      "success-foreground": "oklch(0.266 0.065 152)"
    },
    "dark": {
      "success": "oklch(0.423 0.095 152)",
      "success-foreground": "oklch(0.92 0.042 154)"
    }
  },
  "categories": ["subscription"]
}
