{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "paddle-helpers",
  "type": "registry:lib",
  "title": "Paddle helpers",
  "description": "Shared display types, format utilities, and payment method display for Paddle UI components. No SDK dependency — safe to install without @paddle/paddle-js.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/new-york/blocks/paddle-helpers/lib/paddle-types.ts",
      "content": "// ---\n// Checkout and pricing types\n// Data contracts for checkout and pricing components. Populate from Paddle SDK\n// hooks, checkout event callbacks, or any data source with a compatible shape.\n// ---\n\n/**\n * Formatted price data for display in pricing card components.\n *\n * Passed as the `priceData` prop to pricing card components.\n * All fields are pre-formatted strings, ready to render directly.\n *\n * Typically populated by `usePaddlePrices`, but you can construct it manually\n * or map it from any price data source.\n */\nexport type PriceData = {\n  /** Formatted price string, e.g. \"$9.99\" or \"£12.00\" */\n  total: string\n  /** Pre-discount price, e.g. \"$12.99\". Present only when a discount is applied. */\n  originalTotal?: string\n  /** Billing frequency, e.g. \"month\", \"year\", \"3 months\" */\n  interval?: string\n  /** Trial period length, e.g. \"7 days\", \"1 month\" */\n  trialPeriod?: string\n}\n\n/**\n * Normalized payload delivered to the `onComplete` callback when a Paddle\n * checkout transaction completes successfully.\n *\n * This is a subset of the full Paddle checkout event data, containing the\n * fields most commonly needed for post-purchase handling (e.g. sending to\n * your backend, showing a confirmation screen).\n */\nexport type CheckoutCompleteData = {\n  transactionId: string\n  customerId: string\n  customerEmail: string\n}\n\n/**\n * A single line item in the checkout order summary.\n *\n * Monetary values are raw decimal numbers — the component formats them using\n * the parent `CheckoutSummaryData.currency`.\n * Produced by `mapCheckoutEventsToSummary` from `CheckoutEventsData`.\n */\nexport type CheckoutSummaryItem = {\n  name: string\n  priceName?: string\n  quantity: number\n  lineTotal: number\n}\n\n/**\n * Display contract for the `CheckoutSummary` component.\n *\n * Monetary amounts are raw decimal numbers — the component formats them using `currency`.\n * Produced by `mapCheckoutEventsToSummary` from the `CheckoutEventsData` payload\n * emitted by Paddle.js checkout events.\n */\nexport type CheckoutSummaryData = {\n  items: CheckoutSummaryItem[]\n  subtotal: number\n  tax: number\n  total: number\n  discount?: number\n  /** ISO 4217 currency code, e.g. \"USD\" */\n  currency: string\n  /** Recurring total amount as a decimal number */\n  recurringTotal?: number\n  /** Recurring billing interval, e.g. \"month\" */\n  recurringInterval?: string\n  /** Recurring billing frequency, e.g. 1 */\n  recurringFrequency?: number\n  /** Trial period label, e.g. \"7 days\" or \"1 month\" */\n  trialPeriod?: string\n}\n\n// ---\n// Subscription display types\n// Display contracts for subscription management components. Monetary amounts\n// are raw decimal numbers; date values are ISO 8601 strings. Populate from\n// Paddle's subscription API responses combined with formatting utilities.\n// ---\n\n/** Paddle subscription statuses. From `subscription.status`. */\nexport type SubscriptionStatus = \"active\" | \"canceled\" | \"past_due\" | \"paused\" | \"trialing\"\n\n/**\n * A single line item within a subscription.\n *\n * Represents one product/price combination in the subscription.\n * `lineTotal` should come from `recurring_transaction_details.line_items[n].totals.subtotal`\n * as a decimal number (e.g. 29.99).\n */\nexport type SubscriptionStatusItemData = {\n  /** Product name, e.g. \"Pro Plan\" */\n  productName: string\n  /** Optional product description */\n  productDescription?: string\n  /** Optional product image URL */\n  productImageUrl?: string\n  /** Price/tier name, e.g. \"Monthly\" or \"Annual\". From `items[n].price.name`. */\n  priceName?: string\n  quantity: number\n  /**\n   * Unit price as a decimal number, e.g. 99.99.\n   * From `items[n].price.unit_price.amount`. Only rendered when quantity > 1.\n   */\n  unitPrice?: number\n  /**\n   * Line item subtotal as a decimal number (before discount, before tax), e.g. 29.99.\n   * From `recurring_transaction_details.line_items[n].totals.subtotal`.\n   * Discounts are shown as a separate summary line — this field should NOT include them.\n   */\n  lineTotal: number\n}\n\n/**\n * Display contract for the `SubscriptionStatusCard` component.\n *\n * Pass `undefined` to render a skeleton loading state.\n * Monetary amounts are raw decimal numbers — the component formats them using `currency`.\n * Date values are ISO 8601 strings — the component formats them for display.\n *\n * `totalAmount` should come from `recurring_transaction_details.totals.total` —\n * this is the steady-state recurring amount (after discounts and tax).\n */\nexport type SubscriptionStatusData = {\n  /** Paddle subscription ID, e.g. \"sub_01abc...\" */\n  id?: string\n  /** Subscription line items — auto-adapts layout for single vs multi-item */\n  items: SubscriptionStatusItemData[]\n  /** Recurring total as a decimal number, e.g. 49.99 */\n  totalAmount: number\n  /** ISO 4217 currency code, e.g. \"USD\" */\n  currency: string\n  /** Billing interval, e.g. \"month\" or \"year\" */\n  interval: string\n  /** Billing frequency. Defaults to 1. E.g. 3 for \"every 3 months\". */\n  billingFrequency?: number\n  status: SubscriptionStatus\n  /** ISO 8601 subscription start date */\n  startedAt: string\n  /** ISO 8601 next billing date. Absent when paused or canceled. */\n  nextBilledAt?: string\n  /** ISO 8601 cancellation date. Present only when status is \"canceled\". */\n  canceledAt?: string\n  /**\n   * Payment collection mode. From `subscription.collection_mode`.\n   * \"automatic\" = auto-renew via saved payment method, \"manual\" = invoiced.\n   */\n  collectionMode?: \"automatic\" | \"manual\"\n  scheduledChange?: {\n    /** Type of scheduled change */\n    action: \"cancel\" | \"pause\" | \"resume\"\n    /** ISO 8601 date when the change takes effect */\n    effectiveAt: string\n    /** ISO 8601 date when the subscription resumes, only for pause actions with a set resume date */\n    resumeAt?: string\n  }\n  /** Active discount, if any */\n  discount?: {\n    /** Discount savings amount as a decimal number, e.g. 5.00 */\n    savingsAmount: number\n    /** ISO 8601 date when the discount expires. Absent if discount recurs forever. */\n    endsAt?: string\n    /** Discount code, e.g. \"SAVE20\". From `discount.code`. */\n    code?: string\n    /** Discount description for billing summary, e.g. \"-15%\" or \"-$5.00\". API-provided or derived. */\n    description?: string\n  }\n}\n\n/**\n * Display contract for the `SubscriptionAlert` component.\n *\n * The component derives which alert to render from these fields.\n * Date values are ISO 8601 strings — the component formats them for display.\n * Pass `undefined` to render nothing.\n *\n * `updatePaymentMethodUrl` comes from `subscription.management_urls.update_payment_method`.\n * `trialEndsAt` comes from `subscription.items[0].trial_dates.ends_at`.\n */\nexport type SubscriptionAlertData = {\n  status: SubscriptionStatus\n  /** ISO 8601 cancellation date. Present when status is \"canceled\". */\n  canceledAt?: string\n  scheduledChange?: {\n    /** Type of scheduled change */\n    action: \"cancel\" | \"pause\" | \"resume\"\n    /** ISO 8601 date when the change takes effect */\n    effectiveAt: string\n    /** ISO 8601 date when the subscription resumes, only for pause with a set resume date */\n    resumeAt?: string\n  }\n  /** ISO 8601 trial end date. Present when status is \"trialing\". */\n  trialEndsAt?: string\n  /** Portal deep link to update payment method. Null for manual collection. */\n  updatePaymentMethodUrl?: string\n}\n\n/**\n * Next payment details for the `SubscriptionPaymentCard` component.\n *\n * `amount` should come from `next_transaction.details.totals.grand_total`\n * (accounts for credits and adjustments) — pass as a decimal number (e.g. 29.99).\n * `currency` should come from `subscription.currency_code`.\n * `date` should come from `subscription.next_billed_at` as an ISO 8601 string.\n */\nexport type NextPaymentData = {\n  /** Payment amount as a decimal number, e.g. 29.99 */\n  amount: number\n  /** ISO 4217 currency code, e.g. \"USD\" */\n  currency: string\n  /** ISO 8601 date string for the next billing date, e.g. \"2025-02-01T00:00:00Z\" */\n  date: string\n}\n\n/**\n * Payment method details for the `SubscriptionPaymentCard` component.\n *\n * Pass the raw fields from `transaction.payments[0].method_details` directly —\n * the component resolves the display label automatically via `getPaymentMethodDisplay`.\n *\n * Sourced from the most recent completed transaction for the subscription\n * (not the saved payment methods API — see spec for sourcing details).\n *\n * @example\n * // From a completed Paddle transaction:\n * const payment = transaction.payments[0].methodDetails\n * paymentMethod={payment ? {\n *   type: payment.type,\n *   cardBrand: payment.card?.type,\n *   last4: payment.card?.last4,\n * } : undefined}\n */\nexport type PaymentMethodData = {\n  /** Paddle payment method type from `method_details.type`, e.g. \"card\", \"paypal\", \"apple_pay\" */\n  type: string\n  /** Card brand from `method_details.card.type`, e.g. \"visa\", \"mastercard\". Card payments only. */\n  cardBrand?: string\n  /** Last 4 digits from `method_details.card.last4`, e.g. \"4242\". Card payments only. */\n  last4?: string\n  /** Card expiry month (1–12) from `method_details.card.expiry_month`. Card payments only. */\n  expiryMonth?: number\n  /** Card expiry year (4-digit) from `method_details.card.expiry_year`. Card payments only. */\n  expiryYear?: number\n  /**\n   * Optional display label override. When provided, renders as-is instead of\n   * auto-resolving from `type`/`cardBrand`/`last4`. Use for custom formats\n   * (e.g. \"Visa •••• 4242\") or when you've already formatted the label upstream.\n   */\n  label?: string\n}\n\n/**\n * Display contract for the `PlanChangePreview` component.\n *\n * Sourced from the current subscription entity and the response from\n * `PATCH /subscriptions/{id}/preview`. Pass `undefined` to render a skeleton.\n *\n * Monetary amounts are raw decimal numbers — the component formats them using `currency`.\n * Date values are ISO 8601 strings — the component formats them for display.\n * UI labels and contextual messages belong on component props, not here.\n */\nexport type PlanChangePreviewData = {\n  /** ISO 4217 currency code, e.g. \"USD\" */\n  currency: string\n  /** The plan being replaced */\n  currentPlan: {\n    /** Product name of the current plan */\n    productName: string\n    /** Current recurring total as a decimal number, e.g. 9.99 */\n    price: number\n    /** Current billing interval, e.g. \"month\" */\n    interval: string\n    /** Current billing frequency, defaults to 1 */\n    billingFrequency?: number\n  }\n  /** The plan being switched to */\n  newPlan: {\n    /** Product name of the new plan */\n    productName: string\n    /** New recurring total as a decimal number, e.g. 29.99 */\n    price: number\n    /** New billing interval, e.g. \"month\" */\n    interval: string\n    /** New billing frequency, defaults to 1 */\n    billingFrequency?: number\n  }\n  /** Financial impact of the plan change */\n  costImpact: {\n    /**\n     * Direction of the net financial impact.\n     * - `\"charge\"` — customer owes money (upgrade)\n     * - `\"credit\"` — customer receives credit (downgrade)\n     * - `\"none\"` — no financial movement (e.g. `do_not_bill` proration mode,\n     *   trial upgrade, or paused subscription change)\n     */\n    resultDirection: \"credit\" | \"charge\" | \"none\"\n    /** Net financial impact as a decimal number, e.g. 20.00. From `update_summary.result.amount`. */\n    resultAmount: number\n    /** Prorated credit for unused time on old plan as a decimal number. From `update_summary.credit.amount`. */\n    credit?: number\n    /** Prorated charge for new plan as a decimal number. From `update_summary.charge.amount`. */\n    charge?: number\n    /**\n     * Amount charged immediately as a decimal number.\n     * Present for `*_immediately` proration modes. From `immediate_transaction.details.totals.grand_total`.\n     */\n    immediateAmount?: number\n    /**\n     * Next bill total as a decimal number.\n     * Present for `*_next_billing_period` proration modes. From `next_transaction.details.totals.grand_total`.\n     */\n    nextBillAmount?: number\n    /** ISO 8601 next bill date. From `next_transaction.billing_period.starts_at`. */\n    nextBillDate?: string\n    /**\n     * New steady-state recurring total as a decimal number after the change settles.\n     * From `recurring_transaction_details.totals.total`.\n     */\n    newRecurringTotal: number\n    /** New billing interval. From `billing_cycle.interval`. */\n    newBillingInterval: string\n    /** New billing frequency. From `billing_cycle.frequency`. Defaults to 1. */\n    newBillingFrequency?: number\n  }\n  /** Active discount on the subscription. From `subscription.discount`. */\n  discount?: {\n    /** Discount description, e.g. \"20% off\" or \"-$5.00\". From `subscription.discount.description`. */\n    description: string\n    /** ISO 8601 expiry date. Absent if discount recurs forever. From `subscription.discount.ends_at`. */\n    endsAt?: string\n  }\n  /** Pending scheduled change. From `subscription.scheduled_change`. */\n  scheduledChange?: {\n    /** Type of scheduled change */\n    action: \"cancel\" | \"pause\" | \"resume\"\n    /** ISO 8601 date when the change takes effect */\n    effectiveAt: string\n  }\n  /**\n   * Subscription status. From `subscription.status`.\n   * Used by the component to render contextual messaging — e.g. a trial notice\n   * when `status === \"trialing\"` and there is no immediate charge.\n   */\n  subscriptionStatus?: SubscriptionStatus\n  /**\n   * Payment collection mode. From `subscription.collection_mode`.\n   * Used by the component to adapt labels — e.g. \"Invoice amount\" instead of\n   * \"Amount due now\" when `collectionMode === \"manual\"`.\n   */\n  collectionMode?: \"automatic\" | \"manual\"\n}\n\n// ---\n// Plan change breakdown types\n// Display contract for the detailed financial breakdown component.\n// Shows per-transaction line items, tax, proration, and totals.\n// ---\n\n/**\n * A single line item within a transaction preview.\n * Monetary amounts are raw decimal numbers — the component formats them\n * using the parent `PlanChangeBreakdownData.currency`.\n *\n * Maps to `details.line_items[]` (immediate/next transaction) or\n * `line_items[]` (recurring_transaction_details) in the preview response.\n */\nexport type PlanChangeLineItemData = {\n  /** Product name, e.g. \"Pro Plan\". From `line_items[].product.name`. */\n  productName: string\n  /** Item quantity. From `line_items[].quantity`. */\n  quantity: number\n  /** Unit price as a decimal number, e.g. 49.00. From `line_items[].unit_totals.subtotal`. */\n  unitPrice: number\n  /** Line total as a decimal number, e.g. 26.95. From `line_items[].totals.total`. */\n  total: number\n  /** Whether this line item is prorated. Derived from `line_items[].proration !== null`. */\n  isProrated?: boolean\n  /** Proration period label, e.g. \"15 of 31 days\". Derived from `proration.rate` and `proration.billing_period`. */\n  prorationPeriod?: string\n}\n\n/**\n * Totals breakdown for a single transaction section.\n * All values are raw decimal numbers — the component formats them using\n * the parent `PlanChangeBreakdownData.currency`.\n *\n * Maps to `details.totals` (immediate/next) or `totals` (recurring) in the preview response.\n */\nexport type PlanChangeTransactionTotalsData = {\n  /** Subtotal as a decimal number (before discount, tax, and deductions). From `totals.subtotal`. */\n  subtotal: number\n  /** Discount amount as a decimal number. Present when a discount is active. From `totals.discount`. */\n  discount?: number\n  /** Tax amount as a decimal number. From `totals.tax`. */\n  tax: number\n  /** Credit applied to this transaction as a decimal number. From `totals.credit`. */\n  credit?: number\n  /** Surplus credit added to customer balance as a decimal number. From `totals.credit_to_balance`. */\n  creditToBalance?: number\n  /** Total due as a decimal number (after credits). From `totals.grand_total`. */\n  total: number\n}\n\n/**\n * A complete transaction section (immediate, next, or recurring).\n * The section's position within `PlanChangeBreakdownData` determines its\n * role — the component derives titles and descriptions from that context.\n */\nexport type PlanChangeTransactionSectionData = {\n  /**\n   * ISO 8601 billing date for this transaction period.\n   * Only relevant for `nextTransaction` — sourced from `billing_period.starts_at`.\n   * Omitted for immediate and recurring sections.\n   */\n  billingDate?: string\n  lineItems: PlanChangeLineItemData[]\n  totals: PlanChangeTransactionTotalsData\n}\n\n/**\n * Display contract for the `PlanChangeBreakdown` component.\n *\n * Sourced from `PATCH /subscriptions/{id}/preview`. Pass `undefined` to render a skeleton.\n * Shows the full financial detail: per-transaction line items, tax, proration, and totals.\n */\nexport type PlanChangeBreakdownData = {\n  /** ISO 4217 currency code, e.g. \"USD\" */\n  currency: string\n  /** Net financial result of the change. From `update_summary.result`. */\n  result: {\n    /**\n     * Direction of the net result.\n     * - `\"charge\"` — customer owes money (upgrade)\n     * - `\"credit\"` — customer receives credit (downgrade)\n     * - `\"none\"` — no financial movement\n     */\n    direction: \"charge\" | \"credit\" | \"none\"\n    /** Net amount as a decimal number, e.g. 17.45. From `update_summary.result.amount`. */\n    amount: number\n  }\n  /** Credit/charge breakdown from `update_summary` */\n  breakdown?: {\n    /** Credit from current plan as a decimal number. From `update_summary.credit`. */\n    credit?: number\n    /** Charge for new plan as a decimal number. From `update_summary.charge`. */\n    charge?: number\n  }\n  /**\n   * Immediate transaction details.\n   * Present for `*_immediately` proration modes. From `immediate_transaction`.\n   */\n  immediateTransaction?: PlanChangeTransactionSectionData\n  /** Next billing period transaction details. From `next_transaction`. */\n  nextTransaction?: PlanChangeTransactionSectionData\n  /** Recurring (steady-state) billing details. From `recurring_transaction_details`. */\n  recurringTransaction?: PlanChangeTransactionSectionData\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/new-york/blocks/paddle-helpers/lib/paddle-format.ts",
      "content": "/**\n * Formats an ISO 8601 date string as a locale-aware date string.\n *\n * @param isoString - ISO 8601 date string (e.g. \"2024-01-15T00:00:00Z\")\n * @param locale - Optional BCP 47 locale tag (e.g. \"en-GB\"). Defaults to \"en-US\" so SSR\n *   and client produce identical output (required for React hydration).\n * @returns Formatted date string, e.g. \"Jan 15, 2024\"\n *\n * @example\n * formatDate(\"2024-01-15T00:00:00Z\") // \"Jan 15, 2024\"\n * formatDate(\"2024-01-15T00:00:00Z\", \"en-GB\") // \"15 Jan 2024\"\n */\nexport function formatDate(isoString: string, locale: string = \"en-US\"): string {\n  return new Intl.DateTimeFormat(locale, {\n    year: \"numeric\",\n    month: \"short\",\n    day: \"numeric\",\n  }).format(new Date(isoString))\n}\n\n// Structural alias — accepts both CheckoutEventsTimePeriod and TimePeriod\ntype TimePeriodLike = {\n  frequency: number\n  interval: string\n}\n\n/**\n * Formats a billing cycle for display.\n * @param billingCycle - The billing cycle to format\n * @returns Formatted string like \"month\", \"year\", or \"3 months\", or undefined if no billing cycle\n *\n * @example\n * formatBillingCycle({ frequency: 1, interval: \"month\" }) // \"month\"\n * formatBillingCycle({ frequency: 3, interval: \"month\" }) // \"3 months\"\n */\nexport function formatBillingCycle(\n  billingCycle: TimePeriodLike | null | undefined\n): string | undefined {\n  if (!billingCycle) {\n    return undefined\n  }\n\n  const { frequency, interval } = billingCycle\n  return frequency === 1 ? interval : `${frequency} ${interval}s`\n}\n\n/**\n * Formats a trial period for display.\n * @param trialPeriod - The trial period to format\n * @returns Formatted string like \"7 days\" or \"1 month\"\n *\n * @example\n * formatTrialPeriod({ frequency: 7, interval: \"day\" }) // \"7 days\"\n * formatTrialPeriod({ frequency: 1, interval: \"month\" }) // \"1 month\"\n */\nexport function formatTrialPeriod(trialPeriod: TimePeriodLike): string {\n  const interval = trialPeriod.frequency === 1 ? trialPeriod.interval : `${trialPeriod.interval}s`\n\n  return `${trialPeriod.frequency} ${interval}`\n}\n\n/**\n * Formats a numeric monetary amount as a localised currency string.\n *\n * @param amount - Raw numeric amount (e.g. from Paddle checkout event totals)\n * @param currencyCode - ISO 4217 currency code (e.g. \"USD\", \"GBP\")\n * @param locale - Optional BCP 47 locale tag (e.g. \"en-GB\"). Defaults to \"en-US\" so SSR\n *   and client produce identical output (required for React hydration).\n * @returns Formatted currency string, e.g. \"$29.99\" or \"£12.00\"\n *\n * @example\n * formatMoney(29.99, \"USD\") // \"$29.99\"\n * formatMoney(12, \"GBP\", \"en-GB\") // \"£12.00\"\n */\nexport function formatMoney(\n  amount: number,\n  currencyCode: string,\n  locale: string = \"en-US\"\n): string {\n  return new Intl.NumberFormat(locale, {\n    style: \"currency\",\n    currency: currencyCode,\n  }).format(amount)\n}\n\n// Paddle-supported zero-decimal currencies — amounts are already whole units.\n// https://developer.paddle.com/concepts/payment-methods/currencies\nconst ZERO_DECIMAL_CURRENCIES = new Set([\"JPY\", \"KRW\", \"VND\", \"CLP\"])\n\n/**\n * Parses a Paddle API monetary amount string (lowest denomination, e.g. \"1500\")\n * to a decimal number suitable for `formatMoney` (e.g. 15.00).\n *\n * Paddle returns amounts as strings in the lowest denomination of the currency.\n * Standard currencies (USD, EUR, GBP, etc.) use cents — divide by 100.\n * Zero-decimal currencies (JPY, KRW, VND, CLP) are already whole units — no division.\n *\n * @param raw - Amount string in lowest denomination (e.g. \"1500\" for $15.00 USD or ¥1500 JPY)\n * @param currencyCode - ISO 4217 currency code used to determine decimal handling\n * @returns Decimal number ready for `formatMoney`\n *\n * @example\n * parseAmount(\"1500\", \"USD\") // 15\n * parseAmount(\"999\", \"USD\")  // 9.99\n * parseAmount(\"1500\", \"JPY\") // 1500\n */\nexport function parseAmount(raw: string, currencyCode: string): number {\n  const value = parseInt(raw, 10)\n  if (ZERO_DECIMAL_CURRENCIES.has(currencyCode.toUpperCase())) {\n    return value\n  }\n  return value / 100\n}\n\n/**\n * Returns a human-readable label for a Paddle proration billing mode.\n *\n * @param mode - Paddle `proration_billing_mode` value\n * @returns Display label, e.g. \"Charge prorated amount now\"\n *\n * @example\n * formatProrationMode(\"prorated_immediately\") // \"Charge prorated amount now\"\n * formatProrationMode(\"full_next_billing_period\") // \"Full charge at next billing\"\n */\nexport function formatProrationMode(mode: string): string {\n  const labels: Record<string, string> = {\n    prorated_immediately: \"Charge prorated amount now\",\n    full_immediately: \"Charge full amount now\",\n    prorated_next_billing_period: \"Prorated at next billing\",\n    full_next_billing_period: \"Full charge at next billing\",\n    do_not_bill: \"No charge\",\n  }\n  return labels[mode] ?? mode\n}\n\nconst INTERVAL_LABELS: Record<string, { noun: string; adjective: string }> = {\n  day: { noun: \"Daily\", adjective: \"daily\" },\n  week: { noun: \"Weekly\", adjective: \"weekly\" },\n  month: { noun: \"Monthly\", adjective: \"monthly\" },\n  year: { noun: \"Annually\", adjective: \"annual\" },\n}\n\n/**\n * Returns a human-readable label for a Paddle billing interval.\n *\n * @param interval - Paddle interval key, e.g. \"month\", \"year\"\n * @param style - \"noun\" for toggle labels (\"Monthly\"), \"adjective\" for inline labels (\"monthly\")\n * @returns Display label, e.g. \"Monthly\" or \"monthly\"\n *\n * @example\n * formatIntervalLabel(\"month\")             // \"Monthly\"\n * formatIntervalLabel(\"year\", \"adjective\") // \"annual\"\n * formatIntervalLabel(\"month\", \"noun\")     // \"Monthly\"\n */\nexport function formatIntervalLabel(\n  interval: string,\n  style: \"noun\" | \"adjective\" = \"noun\"\n): string {\n  const entry = INTERVAL_LABELS[interval]\n  if (entry) return entry[style]\n  return interval.charAt(0).toUpperCase() + interval.slice(1)\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/new-york/blocks/paddle-helpers/lib/paddle-payment-method-display.ts",
      "content": "/**\n * Maps Paddle payment method types and card brands to human-readable display labels.\n * Sourced from `transaction.payments[0].method_details` on completed transactions.\n */\n\n/** Paddle payment method types from `transaction.payments[0].method_details.type` */\nexport type PaddlePaymentMethodType =\n  | \"alipay\"\n  | \"apple_pay\"\n  | \"bancontact\"\n  | \"blik\"\n  | \"card\"\n  | \"google_pay\"\n  | \"ideal\"\n  | \"kakao_pay\"\n  | \"south_korea_local_card\"\n  | \"mb_way\"\n  | \"naver_pay\"\n  | \"offline\"\n  | \"payco\"\n  | \"paypal\"\n  | \"pix\"\n  | \"samsung_pay\"\n  | \"unknown\"\n  | \"upi\"\n  | \"wechat_pay\"\n  | \"wire_transfer\"\n  /** @deprecated Returned on historical transactions but replaced by `south_korea_local_card` in newer transactions */\n  | \"korea_local\"\n  | (string & {})\n\n/** All Paddle card brand types from `transaction.payments[0].method_details.card.type` */\nexport type PaddleCardType =\n  | \"american_express\"\n  | \"diners_club\"\n  | \"discover\"\n  | \"jcb\"\n  | \"mada\"\n  | \"maestro\"\n  | \"mastercard\"\n  | \"union_pay\"\n  | \"unknown\"\n  | \"visa\"\n  | (string & {})\n\nconst PAYMENT_METHOD_LABELS: Record<string, string> = {\n  alipay: \"Alipay\",\n  apple_pay: \"Apple Pay\",\n  bancontact: \"Bancontact\",\n  blik: \"BLIK\",\n  card: \"Card\",\n  google_pay: \"Google Pay\",\n  ideal: \"iDEAL\",\n  kakao_pay: \"Kakao Pay\",\n  south_korea_local_card: \"Korea local card\",\n  mb_way: \"MB WAY\",\n  naver_pay: \"Naver Pay\",\n  offline: \"Offline\",\n  payco: \"PAYCO\",\n  paypal: \"PayPal\",\n  pix: \"Pix\",\n  samsung_pay: \"Samsung Pay\",\n  unknown: \"Payment method\",\n  upi: \"UPI\",\n  wechat_pay: \"WeChat Pay\",\n  wire_transfer: \"Wire transfer\",\n  korea_local: \"Korean payment methods\",\n}\n\nconst CARD_BRAND_LABELS: Record<string, string> = {\n  american_express: \"American Express\",\n  diners_club: \"Diners Club\",\n  discover: \"Discover\",\n  jcb: \"JCB\",\n  mada: \"Mada\",\n  maestro: \"Maestro\",\n  mastercard: \"Mastercard\",\n  union_pay: \"UnionPay\",\n  unknown: \"Card\",\n  visa: \"Visa\",\n}\n\nfunction formatUnknownType(type: string): string {\n  return type\n    .split(\"_\")\n    .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n    .join(\" \")\n}\n\n/**\n * Returns the full display label for a Paddle payment method.\n *\n * For card payments, pass `cardBrand` (from `method_details.card.type`) to\n * get the brand name, and `last4` (from `method_details.card.last4`) to\n * append the masked number. Both are optional — the label degrades gracefully.\n *\n * @param type - Paddle payment method type from `method_details.type`\n * @param cardBrand - Card brand from `method_details.card.type` (card payments only)\n * @param last4 - Last 4 digits from `method_details.card.last4` (card payments only)\n * @returns Human-readable payment method label\n *\n * @example\n * getPaymentMethodDisplay(\"card\", \"visa\", \"4242\")    // \"Visa ending 4242\"\n * getPaymentMethodDisplay(\"card\", \"mastercard\")       // \"Mastercard\"\n * getPaymentMethodDisplay(\"card\")                     // \"Card\"\n * getPaymentMethodDisplay(\"paypal\")                   // \"PayPal\"\n * getPaymentMethodDisplay(\"apple_pay\")                // \"Apple Pay\"\n */\nexport function getPaymentMethodDisplay(\n  type: PaddlePaymentMethodType,\n  cardBrand?: PaddleCardType,\n  last4?: string\n): string {\n  if (type === \"card\") {\n    const brand = cardBrand\n      ? (CARD_BRAND_LABELS[cardBrand] ?? formatUnknownType(cardBrand))\n      : \"Card\"\n    return last4 ? `${brand} ending ${last4}` : brand\n  }\n  return PAYMENT_METHOD_LABELS[type] ?? formatUnknownType(type)\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/new-york/blocks/paddle-helpers/lib/payment-method-icons.tsx",
      "content": "import * as React from \"react\"\nimport type { PaddlePaymentMethodType, PaddleCardType } from \"./paddle-payment-method-display\"\n\n// --- Brand icon components ---\n\nfunction VisaIcon({ className }: { className?: string }) {\n  return (\n    <svg viewBox=\"0 0 38 24\" className={className} aria-hidden=\"true\">\n      <rect width=\"38\" height=\"24\" rx=\"4\" fill=\"currentColor\" fillOpacity=\"0.08\" />\n      <path\n        d=\"M16.5 7.5L14.1 16.5H11.7L14.1 7.5H16.5ZM26.4 13.5L27.6 10.1L28.3 13.5H26.4ZM29.1 16.5H31.3L29.4 7.5H27.4C26.9 7.5 26.5 7.8 26.3 8.2L22.8 16.5H25.3L25.8 15.1H28.8L29.1 16.5ZM23.1 13.5C23.1 11.1 19.7 11 19.7 9.9C19.7 9.6 20 9.2 20.7 9.1C21 9.1 22 9 23.1 9.5L23.5 7.8C22.9 7.6 22.1 7.4 21.1 7.4C18.7 7.4 17.1 8.7 17.1 10.5C17.1 11.9 18.4 12.6 19.3 13.1C20.3 13.5 20.6 13.8 20.6 14.2C20.6 14.8 19.9 15.1 19.2 15.1C18 15.1 17.3 14.8 16.7 14.5L16.3 16.3C16.9 16.6 18 16.8 19.1 16.8C21.6 16.8 23.1 15.5 23.1 13.5Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  )\n}\n\nfunction MastercardIcon({ className }: { className?: string }) {\n  return (\n    <svg viewBox=\"0 0 38 24\" className={className} aria-hidden=\"true\">\n      <rect width=\"38\" height=\"24\" rx=\"4\" fill=\"currentColor\" fillOpacity=\"0.08\" />\n      <circle cx=\"15\" cy=\"12\" r=\"5\" fill=\"currentColor\" fillOpacity=\"0.6\" />\n      <circle cx=\"23\" cy=\"12\" r=\"5\" fill=\"currentColor\" fillOpacity=\"0.4\" />\n    </svg>\n  )\n}\n\nfunction AmexIcon({ className }: { className?: string }) {\n  return (\n    <svg viewBox=\"0 0 38 24\" className={className} aria-hidden=\"true\">\n      <rect width=\"38\" height=\"24\" rx=\"4\" fill=\"currentColor\" fillOpacity=\"0.08\" />\n      <text\n        x=\"19\"\n        y=\"16\"\n        textAnchor=\"middle\"\n        fontSize=\"8\"\n        fontWeight=\"700\"\n        fill=\"currentColor\"\n        fontFamily=\"sans-serif\"\n      >\n        AMEX\n      </text>\n    </svg>\n  )\n}\n\nfunction PayPalIcon({ className }: { className?: string }) {\n  return (\n    <svg viewBox=\"0 0 38 24\" className={className} aria-hidden=\"true\">\n      <rect width=\"38\" height=\"24\" rx=\"4\" fill=\"currentColor\" fillOpacity=\"0.08\" />\n      <text\n        x=\"19\"\n        y=\"16\"\n        textAnchor=\"middle\"\n        fontSize=\"7\"\n        fontWeight=\"700\"\n        fill=\"currentColor\"\n        fontFamily=\"sans-serif\"\n      >\n        PayPal\n      </text>\n    </svg>\n  )\n}\n\nfunction ApplePayIcon({ className }: { className?: string }) {\n  return (\n    <svg viewBox=\"0 0 38 24\" className={className} aria-hidden=\"true\">\n      <rect width=\"38\" height=\"24\" rx=\"4\" fill=\"currentColor\" fillOpacity=\"0.08\" />\n      <path\n        d=\"M19 7.5C18 7.5 17.2 8 16.7 8.7C16.2 8.1 15.4 7.5 14.4 7.5C12.6 7.5 11.2 9 11.2 11C11.2 14 14.4 16.5 15.6 16.5C16.1 16.5 16.6 16.2 17 16.2C17.4 16.2 17.9 16.5 18.4 16.5C19.6 16.5 22.8 14 22.8 11C22.8 9 21.4 7.5 19 7.5Z\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.7\"\n      />\n    </svg>\n  )\n}\n\nfunction GooglePayIcon({ className }: { className?: string }) {\n  return (\n    <svg viewBox=\"0 0 38 24\" className={className} aria-hidden=\"true\">\n      <rect width=\"38\" height=\"24\" rx=\"4\" fill=\"currentColor\" fillOpacity=\"0.08\" />\n      <text\n        x=\"19\"\n        y=\"16\"\n        textAnchor=\"middle\"\n        fontSize=\"7\"\n        fontWeight=\"700\"\n        fill=\"currentColor\"\n        fontFamily=\"sans-serif\"\n      >\n        GPay\n      </text>\n    </svg>\n  )\n}\n\n// --- Lookup ---\n\nconst CARD_BRAND_ICONS: Record<string, React.ElementType> = {\n  visa: VisaIcon,\n  mastercard: MastercardIcon,\n  american_express: AmexIcon,\n}\n\nconst PAYMENT_METHOD_ICONS: Record<string, React.ElementType> = {\n  paypal: PayPalIcon,\n  apple_pay: ApplePayIcon,\n  google_pay: GooglePayIcon,\n}\n\n/**\n * Returns an icon component for the given Paddle payment method type and optional card brand.\n * Returns `null` when no branded icon is available — callers should fall back to a generic icon.\n */\nexport function getPaymentMethodIcon(\n  type: PaddlePaymentMethodType,\n  cardBrand?: PaddleCardType\n): React.ElementType | null {\n  if (type === \"card\" && cardBrand) {\n    return CARD_BRAND_ICONS[cardBrand] ?? null\n  }\n  return PAYMENT_METHOD_ICONS[type] ?? null\n}\n",
      "type": "registry:lib"
    }
  ]
}
