{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "plan-change-preview",
  "type": "registry:component",
  "title": "Plan change preview",
  "description": "Compact plan change summary card with side-by-side plan comparison, proration breakdown, upgrade/downgrade badge, discount, scheduled change warning, and optional confirm/cancel actions. Decision-support view for plan change flows.",
  "dependencies": ["lucide-react"],
  "registryDependencies": [
    "@paddle/paddle-helpers",
    "card",
    "badge",
    "skeleton",
    "separator",
    "alert"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/plan-change-preview/components/plan-change-preview.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowRight, AlertCircle } 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 { Alert, AlertDescription } from \"@/registry/new-york/ui/alert\"\nimport { cn } from \"@/lib/utils\"\nimport type { PlanChangePreviewData } from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-types\"\nimport {\n  formatMoney,\n  formatDate,\n  formatBillingCycle,\n  formatProrationMode,\n} from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-format\"\n\n/** Props for the `PlanChangePreview` component. */\nexport type PlanChangePreviewProps = {\n  preview?: PlanChangePreviewData\n  /**\n   * Paddle proration billing mode passed to `PATCH /subscriptions/{id}/preview`.\n   * The component derives the billing row label and effective date from this value.\n   */\n  prorationBillingMode?:\n    | \"prorated_immediately\"\n    | \"full_immediately\"\n    | \"prorated_next_billing_period\"\n    | \"full_next_billing_period\"\n    | \"do_not_bill\"\n  className?: string\n}\n\nexport function PlanChangePreview({\n  preview,\n  prorationBillingMode,\n  className,\n}: PlanChangePreviewProps) {\n  if (!preview) {\n    return <PlanChangePreviewSkeleton className={className} />\n  }\n\n  const {\n    currency,\n    currentPlan,\n    newPlan,\n    costImpact,\n    discount,\n    scheduledChange,\n    subscriptionStatus,\n    collectionMode,\n  } = preview\n  const isCharge = costImpact.resultDirection === \"charge\"\n  const isCredit = costImpact.resultDirection === \"credit\"\n  const isNeutral = costImpact.resultDirection === \"none\"\n  const isManual = collectionMode === \"manual\"\n  const isTrialing = subscriptionStatus === \"trialing\"\n\n  const changeType = isCharge ? \"upgrade\" : isCredit ? \"downgrade\" : \"change\"\n\n  const prorationLabel = prorationBillingMode\n    ? formatProrationMode(prorationBillingMode)\n    : undefined\n\n  const isImmediate =\n    prorationBillingMode?.includes(\"immediately\") || prorationBillingMode === \"do_not_bill\"\n\n  function resolveEffectiveDate(): string | undefined {\n    if (prorationBillingMode) {\n      if (isImmediate) return \"Immediately\"\n      return costImpact.nextBillDate ? formatDate(costImpact.nextBillDate) : undefined\n    }\n    if (costImpact.immediateAmount !== undefined) return \"Immediately\"\n    return costImpact.nextBillDate ? formatDate(costImpact.nextBillDate) : undefined\n  }\n  const effectiveDate = resolveEffectiveDate()\n\n  function resolveScheduledChangeMessage(): string | undefined {\n    if (!scheduledChange) return undefined\n    if (scheduledChange.action === \"resume\") return undefined\n    const actionLabel = scheduledChange.action === \"cancel\" ? \"Cancellation\" : \"Pause\"\n    return `${actionLabel} scheduled for ${formatDate(scheduledChange.effectiveAt)}. Billing options may be restricted.`\n  }\n  const scheduledChangeMessage = resolveScheduledChangeMessage()\n\n  const hasBreakdownRows = costImpact.credit !== undefined || costImpact.charge !== undefined\n\n  const totalLabel = isCredit\n    ? \"Credit to account\"\n    : isNeutral\n      ? \"No charge\"\n      : isManual\n        ? \"Invoice amount\"\n        : costImpact.immediateAmount !== undefined\n          ? \"Amount due now\"\n          : \"Amount at next billing\"\n\n  const currentIntervalLabel =\n    formatBillingCycle({\n      interval: currentPlan.interval,\n      frequency: currentPlan.billingFrequency ?? 1,\n    }) ?? currentPlan.interval\n\n  const newIntervalLabel =\n    formatBillingCycle({\n      interval: newPlan.interval,\n      frequency: newPlan.billingFrequency ?? 1,\n    }) ?? newPlan.interval\n\n  return (\n    <Card className={cn(\"gap-4\", className)}>\n      <CardHeader>\n        <CardTitle className=\"text-base font-semibold\">Change summary</CardTitle>\n        <CardDescription>Review the overview of this change</CardDescription>\n      </CardHeader>\n\n      <CardContent className=\"space-y-4\">\n        {scheduledChangeMessage && (\n          <Alert>\n            <AlertCircle className=\"size-4\" />\n            <AlertDescription>{scheduledChangeMessage}</AlertDescription>\n          </Alert>\n        )}\n\n        <div className=\"flex items-stretch gap-3\">\n          <div className=\"flex-1 min-w-0 rounded-lg border bg-muted/40 p-3\">\n            <div className=\"text-xs text-muted-foreground mb-1\">Current plan</div>\n            <div className=\"font-medium text-sm truncate\">{currentPlan.productName}</div>\n            <div className=\"text-muted-foreground text-sm\">\n              {formatMoney(currentPlan.price, currency)}\n              <span className=\"text-xs\"> / {currentIntervalLabel}</span>\n            </div>\n          </div>\n\n          <div className=\"flex items-center shrink-0\">\n            <ArrowRight className=\"size-4 text-muted-foreground\" />\n          </div>\n\n          <div className=\"flex-1 min-w-0 rounded-lg border bg-primary/5 border-primary/20 p-3\">\n            <div className=\"text-xs text-muted-foreground mb-1\">New plan</div>\n            <div className=\"font-medium text-sm truncate\">{newPlan.productName}</div>\n            <div className=\"text-muted-foreground text-sm\">\n              {formatMoney(newPlan.price, currency)}\n              <span className=\"text-xs\"> / {newIntervalLabel}</span>\n            </div>\n          </div>\n        </div>\n\n        <Separator />\n\n        <div className=\"space-y-2\">\n          <div className=\"flex items-center justify-between text-sm\">\n            <span className=\"text-muted-foreground\">Change type</span>\n            <Badge\n              variant={\n                changeType === \"upgrade\"\n                  ? \"default\"\n                  : changeType === \"downgrade\"\n                    ? \"secondary\"\n                    : \"outline\"\n              }\n            >\n              {changeType === \"upgrade\"\n                ? \"Upgrade\"\n                : changeType === \"downgrade\"\n                  ? \"Downgrade\"\n                  : \"Change\"}\n            </Badge>\n          </div>\n\n          {prorationLabel && (\n            <div className=\"flex items-center justify-between text-sm\">\n              <span className=\"text-muted-foreground\">Billing</span>\n              <span className=\"text-right\">{prorationLabel}</span>\n            </div>\n          )}\n\n          {effectiveDate && (\n            <div className=\"flex items-center justify-between text-sm\">\n              <span className=\"text-muted-foreground\">Effective</span>\n              <span>{effectiveDate}</span>\n            </div>\n          )}\n\n          {discount && (\n            <div className=\"flex items-center justify-between text-sm\">\n              <span className=\"text-muted-foreground\">Discount</span>\n              <span className=\"text-right\">\n                <span className=\"text-success-foreground\">{discount.description}</span>\n                {discount.endsAt && (\n                  <span className=\"text-muted-foreground text-xs block\">\n                    until {formatDate(discount.endsAt)}\n                  </span>\n                )}\n              </span>\n            </div>\n          )}\n        </div>\n\n        <Separator />\n\n        <div className=\"space-y-1.5\">\n          <div className=\"flex items-center justify-between text-sm\">\n            <span className=\"text-muted-foreground\">Current</span>\n            <span>\n              {formatMoney(currentPlan.price, currency)}\n              <span className=\"text-muted-foreground text-xs\"> / {currentIntervalLabel}</span>\n            </span>\n          </div>\n          <div className=\"flex items-center justify-between text-sm\">\n            <span className=\"text-muted-foreground\">New</span>\n            <span>\n              {formatMoney(newPlan.price, currency)}\n              <span className=\"text-muted-foreground text-xs\"> / {newIntervalLabel}</span>\n            </span>\n          </div>\n        </div>\n\n        {/* Financial summary — credit/charge breakdown + total row */}\n        {(hasBreakdownRows || !isNeutral) && (\n          <>\n            <Separator />\n            <div className=\"space-y-1.5\">\n              {costImpact.credit !== undefined && (\n                <div className=\"flex items-center justify-between text-sm text-success-foreground\">\n                  <span>Credit</span>\n                  <span>−{formatMoney(costImpact.credit, currency)}</span>\n                </div>\n              )}\n              {costImpact.charge !== undefined && (\n                <div className=\"flex items-center justify-between text-sm\">\n                  <span className=\"text-muted-foreground\">Charge</span>\n                  <span>{formatMoney(costImpact.charge, currency)}</span>\n                </div>\n              )}\n              <div\n                className={cn(\n                  \"flex items-center justify-between font-medium\",\n                  hasBreakdownRows && \"pt-1.5 border-t\"\n                )}\n              >\n                <span>{totalLabel}</span>\n                <span className={cn(isCredit && \"text-success-foreground\")}>\n                  {isCredit ? \"−\" : \"\"}\n                  {formatMoney(costImpact.resultAmount, currency)}\n                </span>\n              </div>\n            </div>\n          </>\n        )}\n\n        {/* Contextual notes derived from subscription state */}\n        {isTrialing && isNeutral && (\n          <p className=\"text-xs text-muted-foreground\">\n            No charges during your trial. Billing begins when your trial ends.\n          </p>\n        )}\n        {isManual && isCharge && (\n          <p className=\"text-xs text-muted-foreground\">\n            An invoice will be created for this amount.\n          </p>\n        )}\n      </CardContent>\n    </Card>\n  )\n}\n\nfunction PlanChangePreviewSkeleton({ className }: { className?: string }) {\n  return (\n    <Card className={cn(\"gap-4\", className)}>\n      <CardHeader>\n        <Skeleton className=\"h-4 w-40\" />\n        <Skeleton className=\"h-3 w-52\" />\n      </CardHeader>\n      <CardContent className=\"space-y-4\">\n        <div className=\"flex items-stretch gap-3\">\n          <div className=\"flex-1 rounded-lg border bg-muted/40 p-3 space-y-2\">\n            <Skeleton className=\"h-3 w-20\" />\n            <Skeleton className=\"h-4 w-24\" />\n            <Skeleton className=\"h-3 w-16\" />\n          </div>\n          <div className=\"flex items-center\">\n            <Skeleton className=\"h-4 w-4 rounded\" />\n          </div>\n          <div className=\"flex-1 rounded-lg border p-3 space-y-2\">\n            <Skeleton className=\"h-3 w-16\" />\n            <Skeleton className=\"h-4 w-20\" />\n            <Skeleton className=\"h-3 w-16\" />\n          </div>\n        </div>\n        <Separator />\n        <div className=\"space-y-2\">\n          <div className=\"flex justify-between\">\n            <Skeleton className=\"h-4 w-20\" />\n            <Skeleton className=\"h-5 w-16\" />\n          </div>\n          <div className=\"flex justify-between\">\n            <Skeleton className=\"h-4 w-16\" />\n            <Skeleton className=\"h-4 w-32\" />\n          </div>\n        </div>\n        <Separator />\n        <div className=\"space-y-1.5\">\n          <div className=\"flex justify-between\">\n            <Skeleton className=\"h-4 w-16\" />\n            <Skeleton className=\"h-4 w-20\" />\n          </div>\n          <div className=\"flex justify-between\">\n            <Skeleton className=\"h-4 w-8\" />\n            <Skeleton className=\"h-4 w-20\" />\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/new-york/blocks/plan-change-preview/components/paddle-plan-change-preview.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { mapPreviewToPlanChangeData } from \"@/registry/new-york/blocks/plan-change-preview/lib/plan-change-preview-utils\"\nimport { PlanChangePreview, type PlanChangePreviewProps } from \"./plan-change-preview\"\n\ntype PaddleSubscription = Parameters<typeof mapPreviewToPlanChangeData>[0]\ntype PreviewResponse = Parameters<typeof mapPreviewToPlanChangeData>[1]\ntype PaddleDiscount = Parameters<typeof mapPreviewToPlanChangeData>[2]\n\nexport type PaddlePlanChangePreviewProps = {\n  /** Current Paddle Subscription (before the change) */\n  subscription: PaddleSubscription\n  /** Subscription update preview response with the proposed changes */\n  previewResponse: PreviewResponse\n  /** Paddle Discount for enriched discount display */\n  discount?: PaddleDiscount\n} & Omit<PlanChangePreviewProps, \"preview\">\n\n/**\n * Paddle-aware wrapper for `PlanChangePreview`.\n *\n * Accepts the current subscription entity and the Paddle preview update response,\n * maps them to `PlanChangePreviewData`, and renders the UI component.\n *\n * Pass `prorationBillingMode` through to control which billing row and label\n * the component displays.\n *\n * `subscription.items[].price.unit_price` must be present for the current plan\n * price to display correctly — fetch with full price details, not a list response.\n *\n * @example\n * <PaddlePlanChangePreview\n *   subscription={subscription}\n *   previewResponse={previewResponse}\n *   discount={discount}\n *   prorationBillingMode=\"prorated_immediately\"\n * />\n */\nexport function PaddlePlanChangePreview({\n  subscription,\n  previewResponse,\n  discount,\n  ...uiProps\n}: PaddlePlanChangePreviewProps) {\n  const preview = mapPreviewToPlanChangeData(subscription, previewResponse, discount)\n  return <PlanChangePreview preview={preview} {...uiProps} />\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/new-york/blocks/plan-change-preview/lib/plan-change-preview-utils.ts",
      "content": "import type { PlanChangePreviewData } 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 — minimal types matching the Paddle Node SDK / API response.\n// The preview endpoint (PATCH /subscriptions/{id}/preview) returns a full\n// subscription snapshot plus update_summary and transaction previews.\n// ---\n\ntype AmountWithCurrency = {\n  amount: string\n  currencyCode: string\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 TransactionPreview = {\n  billingPeriod?: { startsAt?: string | null } | null\n  details?: {\n    totals?: {\n      grandTotal?: string | null\n    } | null\n  } | null\n} | null\n\ntype RecurringDetails = {\n  totals: { total: string; currencyCode: string }\n  lineItems?: Array<{ totals: { total: string } }> | null\n}\n\ntype PlanItem = {\n  product: { name: string }\n  price: {\n    unitPrice?: { amount: string; currencyCode: string } | null\n    billingCycle?: { interval: string; frequency: number } | null\n  }\n}\n\ntype SubscriptionPreviewResponse = {\n  currencyCode: string\n  billingCycle: { interval: string; frequency: number }\n  updateSummary: UpdateSummary\n  immediateTransaction?: TransactionPreview\n  nextTransaction?: TransactionPreview\n  recurringTransactionDetails: RecurringDetails\n  items: PlanItem[]\n}\n\n// Matches SDK SubscriptionDiscount — subscription.discount only has id + dates\ntype SubscriptionDiscountField = {\n  id: string\n  startsAt?: string | null\n  endsAt?: string | null\n} | null\n\ntype PaddleSubscription = {\n  currencyCode: string\n  billingCycle: { interval: string; frequency: number }\n  items: PlanItem[]\n  status?: \"active\" | \"canceled\" | \"past_due\" | \"paused\" | \"trialing\"\n  collectionMode?: \"automatic\" | \"manual\"\n  scheduledChange?: {\n    action: \"cancel\" | \"pause\" | \"resume\"\n    effectiveAt: string\n  } | null\n  discount?: SubscriptionDiscountField\n}\n\n// Full Discount catalog entity — fetched separately via paddle.discounts.get(id)\ntype PaddleDiscount = {\n  id: string\n  description: string\n  code?: string | null\n  type: \"flat\" | \"flat_per_seat\" | \"percentage\"\n  amount: string\n}\n\n/**\n * Maps a Paddle subscription preview API response to the `PlanChangePreviewData`\n * display contract consumed by `PlanChangePreview`.\n *\n * Pass `prorationBillingMode` directly to the `<PlanChangePreview>` component as a prop.\n * Optionally pass a Paddle `Discount` to enrich the discount display.\n *\n * @param subscription - Current Paddle Subscription (before the change)\n * @param preview - Subscription update preview response\n * @param discount - Paddle Discount for enriched discount display\n * @returns Mapped preview data for `<PlanChangePreview />`\n *\n * @example\n * const subscription = await paddle.subscriptions.get(subscriptionId)\n * const preview = await paddle.subscriptions.previewUpdate(subscriptionId, {\n *   items: [{ priceId: newPriceId, quantity: 1 }],\n *   prorationBillingMode: \"prorated_immediately\",\n * })\n * const data = mapPreviewToPlanChangeData(subscription, preview)\n */\nexport function mapPreviewToPlanChangeData(\n  subscription: PaddleSubscription,\n  preview: SubscriptionPreviewResponse,\n  discount?: PaddleDiscount | null\n): PlanChangePreviewData {\n  const currencyCode = preview.currencyCode\n  const { updateSummary, immediateTransaction, nextTransaction, 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\n  const resultDirection: PlanChangePreviewData[\"costImpact\"][\"resultDirection\"] =\n    resultAmount === 0 ? \"none\" : updateSummary.result.action\n\n  const immediateAmount = immediateTransaction?.details?.totals?.grandTotal\n    ? parseAmount(immediateTransaction.details.totals.grandTotal, currencyCode)\n    : undefined\n\n  const nextBillAmount = nextTransaction?.details?.totals?.grandTotal\n    ? parseAmount(nextTransaction.details.totals.grandTotal, currencyCode)\n    : undefined\n\n  const nextBillDate = nextTransaction?.billingPeriod?.startsAt ?? undefined\n\n  const currentItem = subscription.items[0]\n  const newItem = preview.items[0]\n\n  // description is required by PlanChangePreviewData.discount — only set when full entity provided\n  const discountOutput: PlanChangePreviewData[\"discount\"] = discount\n    ? {\n        description: discount.description,\n        endsAt: subscription.discount?.endsAt ?? undefined,\n      }\n    : undefined\n\n  const scheduledChange: PlanChangePreviewData[\"scheduledChange\"] = subscription.scheduledChange\n    ? {\n        action: subscription.scheduledChange.action,\n        effectiveAt: subscription.scheduledChange.effectiveAt,\n      }\n    : undefined\n\n  return {\n    currency: currencyCode,\n    currentPlan: {\n      productName: currentItem.product.name,\n      price: currentItem.price.unitPrice\n        ? parseAmount(currentItem.price.unitPrice.amount, currencyCode)\n        : 0,\n      interval: subscription.billingCycle.interval,\n      billingFrequency: subscription.billingCycle.frequency,\n    },\n    newPlan: {\n      productName: newItem.product.name,\n      price: parseAmount(recurringTransactionDetails.totals.total, currencyCode),\n      interval: preview.billingCycle.interval,\n      billingFrequency: preview.billingCycle.frequency,\n    },\n    costImpact: {\n      resultDirection,\n      resultAmount,\n      credit: creditAmount > 0 ? creditAmount : undefined,\n      charge: chargeAmount > 0 ? chargeAmount : undefined,\n      immediateAmount,\n      nextBillAmount,\n      nextBillDate,\n      newRecurringTotal: parseAmount(recurringTransactionDetails.totals.total, currencyCode),\n      newBillingInterval: preview.billingCycle.interval,\n      newBillingFrequency: preview.billingCycle.frequency,\n    },\n    discount: discountOutput,\n    scheduledChange,\n    subscriptionStatus: subscription.status,\n    collectionMode: subscription.collectionMode,\n  }\n}\n",
      "type": "registry:lib"
    }
  ],
  "cssVars": {
    "light": {
      "success": "oklch(0.723 0.157 150)",
      "success-foreground": "oklch(0.266 0.065 152)",
      "warning": "oklch(0.769 0.163 70)",
      "warning-foreground": "oklch(0.286 0.066 53)"
    },
    "dark": {
      "success": "oklch(0.423 0.095 152)",
      "success-foreground": "oklch(0.92 0.042 154)",
      "warning": "oklch(0.437 0.096 54)",
      "warning-foreground": "oklch(0.94 0.034 82)"
    }
  },
  "categories": ["subscription"]
}
