{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "subscription-alert",
  "type": "registry:component",
  "title": "Subscription alert",
  "description": "Contextual alerts for subscription events: payment failures, scheduled cancellations, pauses, trials. Derives the correct alert from subscription state automatically.",
  "dependencies": ["lucide-react"],
  "registryDependencies": ["@paddle/paddle-helpers", "alert"],
  "files": [
    {
      "path": "registry/new-york/blocks/subscription-alert/components/subscription-alert.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AlertCircle, Info, TriangleAlert, X } from \"lucide-react\"\nimport { Alert, AlertDescription } from \"@/registry/new-york/ui/alert\"\nimport { cn } from \"@/lib/utils\"\nimport type { SubscriptionAlertData } from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-types\"\nimport {\n  deriveSubscriptionAlert,\n  type AlertVariant,\n} from \"@/registry/new-york/blocks/subscription-alert/lib/subscription-alert-utils\"\n\n/** Props for the `SubscriptionAlert` component. */\nexport type SubscriptionAlertProps = {\n  subscription?: SubscriptionAlertData\n  onDismiss?: () => void\n  className?: string\n}\n\nconst VARIANT_CONFIG: Record<AlertVariant, { icon: React.ElementType; className: string }> = {\n  destructive: {\n    icon: AlertCircle,\n    className: \"border-destructive/50 bg-destructive/15 text-destructive [&>svg]:text-destructive\",\n  },\n  warning: {\n    icon: TriangleAlert,\n    className:\n      \"border-warning/40 bg-warning/15 text-warning-foreground [&>svg]:text-warning-foreground\",\n  },\n  info: {\n    icon: Info,\n    className: \"border-info/40 bg-info/15 text-info-foreground [&>svg]:text-info-foreground\",\n  },\n}\n\nexport function SubscriptionAlert({ subscription, onDismiss, className }: SubscriptionAlertProps) {\n  const alert = deriveSubscriptionAlert(subscription)\n\n  if (!alert) return null\n\n  const config = VARIANT_CONFIG[alert.variant]\n  const Icon = config.icon\n\n  return (\n    <Alert className={cn(\"relative\", config.className, className)}>\n      <Icon className=\"size-4\" />\n      <AlertDescription className=\"flex items-center justify-between gap-4 text-inherit\">\n        <span>\n          {alert.message}\n          {alert.actionUrl && alert.actionLabel && (\n            <>\n              {\" \"}\n              <a\n                href={alert.actionUrl}\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                className=\"font-medium underline underline-offset-2 hover:no-underline\"\n              >\n                {alert.actionLabel}\n              </a>\n            </>\n          )}\n        </span>\n        {onDismiss && (\n          <button\n            onClick={onDismiss}\n            aria-label=\"Dismiss alert\"\n            className=\"shrink-0 rounded-sm opacity-60 hover:opacity-100 transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n          >\n            <X className=\"size-4\" />\n          </button>\n        )}\n      </AlertDescription>\n    </Alert>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/new-york/blocks/subscription-alert/components/paddle-subscription-alert.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { mapSubscriptionToAlertData } from \"@/registry/new-york/blocks/subscription-alert/lib/subscription-alert-utils\"\nimport { SubscriptionAlert, type SubscriptionAlertProps } from \"./subscription-alert\"\n\ntype PaddleSubscription = Parameters<typeof mapSubscriptionToAlertData>[0]\n\nexport type PaddleSubscriptionAlertProps = {\n  subscription: PaddleSubscription\n} & Omit<SubscriptionAlertProps, \"subscription\">\n\n/**\n * Paddle-aware wrapper for `SubscriptionAlert`.\n *\n * Accepts the raw Paddle subscription entity, maps it to `SubscriptionAlertData`,\n * and renders the UI component. All display-only props are passed through directly.\n *\n * @example\n * <PaddleSubscriptionAlert\n *   subscription={subscription}\n *   className=\"mb-4\"\n * />\n */\nexport function PaddleSubscriptionAlert({\n  subscription,\n  ...uiProps\n}: PaddleSubscriptionAlertProps) {\n  const data = mapSubscriptionToAlertData(subscription)\n  return <SubscriptionAlert subscription={data} {...uiProps} />\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/new-york/blocks/subscription-alert/lib/subscription-alert-utils.ts",
      "content": "import type { SubscriptionAlertData } from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-types\"\nimport { formatDate } from \"@/registry/new-york/blocks/paddle-helpers/lib/paddle-format\"\n\nexport type AlertVariant = \"destructive\" | \"warning\" | \"info\"\n\n/**\n * Machine-readable reason identifier for the derived alert.\n *\n * Use this to key i18n translations, apply custom rendering logic, or\n * conditionally render additional UI without parsing the default English message.\n *\n * @example\n * const alert = deriveSubscriptionAlert(data)\n * if (alert) {\n *   const translated = t(`subscription.alert.${alert.reason}`, { date: ... })\n * }\n */\nexport type AlertReason =\n  | \"past_due\" // P1: status === \"past_due\"\n  | \"canceled\" // P2: status === \"canceled\"\n  | \"scheduled_cancel\" // P3: scheduledChange.action === \"cancel\"\n  | \"scheduled_pause\" // P4: scheduledChange.action === \"pause\"\n  | \"paused_resuming\" // P5: status === \"paused\" + scheduledChange.action === \"resume\"\n  | \"paused\" // P6: status === \"paused\", no scheduled resume\n  | \"trialing\" // P7: status === \"trialing\" + trialEndsAt present\n\nexport type DerivedAlert = {\n  variant: AlertVariant\n  /**\n   * Machine-readable reason for this alert. Stable across versions — use to\n   * key i18n translations or apply custom logic without parsing `message`.\n   */\n  reason: AlertReason\n  /** Default English message. Sufficient for most consumers out of the box. */\n  message: string\n  actionLabel?: string\n  actionUrl?: string\n} | null\n\n/**\n * Derives the contextual alert for a subscription state.\n *\n * Evaluated in priority order; first match wins.\n * Returns `null` for healthy active subscriptions.\n *\n * @param data - Subscription alert data\n * @returns Alert descriptor with `reason` + default `message`, or null\n *\n * @example\n * deriveSubscriptionAlert({ status: \"past_due\", updatePaymentMethodUrl: \"https://...\" })\n * // { reason: \"past_due\", variant: \"destructive\", message: \"Payment failed...\", ... }\n *\n * @example i18n usage\n * const alert = deriveSubscriptionAlert(data)\n * if (alert) {\n *   const message = t(`subscription.alert.${alert.reason}`, { effectiveAt: data.scheduledChange?.effectiveAt })\n * }\n */\nexport function deriveSubscriptionAlert(data: SubscriptionAlertData | undefined): DerivedAlert {\n  if (!data) return null\n\n  const { status, canceledAt, scheduledChange, trialEndsAt, updatePaymentMethodUrl } = data\n\n  // Priority 1: past_due\n  if (status === \"past_due\") {\n    return {\n      reason: \"past_due\",\n      variant: \"destructive\",\n      message: updatePaymentMethodUrl\n        ? \"Payment failed. Please update your payment method to avoid losing access.\"\n        : \"Payment failed. Please contact support to resolve your billing issue.\",\n      actionLabel: updatePaymentMethodUrl ? \"Update payment method\" : undefined,\n      actionUrl: updatePaymentMethodUrl,\n    }\n  }\n\n  // Priority 2: canceled\n  if (status === \"canceled\") {\n    return {\n      reason: \"canceled\",\n      variant: \"destructive\",\n      message: canceledAt\n        ? `This subscription was canceled on ${formatDate(canceledAt)}.`\n        : \"This subscription has been canceled.\",\n    }\n  }\n\n  // Priority 3: scheduled_cancel\n  if (scheduledChange?.action === \"cancel\") {\n    return {\n      reason: \"scheduled_cancel\",\n      variant: \"warning\",\n      message: `This subscription is scheduled to cancel on ${formatDate(scheduledChange.effectiveAt)}.`,\n    }\n  }\n\n  // Priority 4: scheduled_pause\n  if (scheduledChange?.action === \"pause\") {\n    const message = scheduledChange.resumeAt\n      ? `This subscription will pause on ${formatDate(scheduledChange.effectiveAt)} and resume on ${formatDate(scheduledChange.resumeAt)}.`\n      : `This subscription will pause on ${formatDate(scheduledChange.effectiveAt)}.`\n    return { reason: \"scheduled_pause\", variant: \"warning\", message }\n  }\n\n  // Priority 5: paused_resuming\n  if (status === \"paused\" && scheduledChange?.action === \"resume\") {\n    return {\n      reason: \"paused_resuming\",\n      variant: \"info\",\n      message: `This subscription is paused. It will resume on ${formatDate(scheduledChange.effectiveAt)}.`,\n    }\n  }\n\n  // Priority 6: paused\n  if (status === \"paused\") {\n    return {\n      reason: \"paused\",\n      variant: \"info\",\n      message: \"This subscription is paused.\",\n    }\n  }\n\n  // Priority 7: trialing\n  if (status === \"trialing\" && trialEndsAt) {\n    return {\n      reason: \"trialing\",\n      variant: \"info\",\n      message: `Your trial ends on ${formatDate(trialEndsAt)}.`,\n    }\n  }\n\n  // Priority 8: active — no alert\n  return null\n}\n\n// ---\n// Mapping utility — Paddle API → SubscriptionAlertData display contract\n// ---\n\ntype PaddleSubscription = {\n  status: \"active\" | \"canceled\" | \"past_due\" | \"paused\" | \"trialing\"\n  canceledAt?: string | null\n  scheduledChange?: {\n    action: \"cancel\" | \"pause\" | \"resume\"\n    effectiveAt: string\n    resumeAt?: string | null\n  } | null\n  items?: Array<{\n    trialDates?: { endsAt?: string | null } | null\n  }>\n  managementUrls?: {\n    updatePaymentMethod?: string | null\n  } | null\n}\n\n/**\n * Maps a Paddle subscription API response to the `SubscriptionAlertData`\n * display contract consumed by `SubscriptionAlert`.\n *\n * @param subscription - Paddle Subscription\n * @returns Mapped alert data for `<SubscriptionAlert />`\n *\n * @example\n * const data = await paddle.subscriptions.get(subscriptionId)\n * const alertData = mapSubscriptionToAlertData(data)\n */\nexport function mapSubscriptionToAlertData(\n  subscription: PaddleSubscription\n): SubscriptionAlertData {\n  return {\n    status: subscription.status,\n    canceledAt: subscription.canceledAt ?? undefined,\n    scheduledChange: subscription.scheduledChange\n      ? {\n          action: subscription.scheduledChange.action,\n          effectiveAt: subscription.scheduledChange.effectiveAt,\n          resumeAt: subscription.scheduledChange.resumeAt ?? undefined,\n        }\n      : undefined,\n    // Trial end date comes from the first item's trial dates\n    trialEndsAt: subscription.items?.[0]?.trialDates?.endsAt ?? undefined,\n    updatePaymentMethodUrl: subscription.managementUrls?.updatePaymentMethod ?? undefined,\n  }\n}\n",
      "type": "registry:lib"
    }
  ],
  "cssVars": {
    "light": {
      "warning": "oklch(0.769 0.163 70)",
      "warning-foreground": "oklch(0.286 0.066 53)",
      "info": "oklch(0.623 0.163 255)",
      "info-foreground": "oklch(0.235 0.063 258)"
    },
    "dark": {
      "warning": "oklch(0.437 0.096 54)",
      "warning-foreground": "oklch(0.94 0.034 82)",
      "info": "oklch(0.423 0.095 258)",
      "info-foreground": "oklch(0.92 0.042 252)"
    }
  },
  "categories": ["subscription"]
}
