{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "billing-plan-selector",
  "title": "Billing Plan Selector",
  "description": "Select subscription plans with monthly/annual billing toggle.",
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "switch"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/billing/billing-plan-selector.tsx",
      "content": "\"use client\";\n\nimport { Check, Loader2 } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Badge } from \"@/registry/new-york/ui/badge\";\nimport { Button } from \"@/registry/new-york/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/registry/new-york/ui/card\";\nimport { Switch } from \"@/registry/new-york/ui/switch\";\n\nexport interface PlanFeature {\n  name: string;\n  included?: boolean;\n  limit?: number | string;\n  tooltip?: string;\n}\n\nexport interface SelectablePlan {\n  id: string;\n  name: string;\n  description?: string;\n  price: {\n    monthly: number;\n    annual: number;\n  };\n  currency?: string;\n  features: PlanFeature[];\n  isPopular?: boolean;\n  isCurrent?: boolean;\n  ctaLabel?: string;\n  disabled?: boolean;\n}\n\nexport interface BillingPlanSelectorProps {\n  plans: SelectablePlan[];\n  selectedPlanId?: string;\n  billingPeriod?: \"monthly\" | \"annual\";\n  onBillingPeriodChange?: (period: \"monthly\" | \"annual\") => void;\n  onPlanSelect?: (planId: string) => void;\n  className?: string;\n  showAnnualSavings?: boolean;\n  currency?: string;\n  layout?: \"grid\" | \"list\";\n}\n\nfunction formatPrice(\n  amount: number,\n  currency = \"USD\",\n  period: \"monthly\" | \"annual\" = \"monthly\"\n): string {\n  const formatter = new Intl.NumberFormat(\"en-US\", {\n    style: \"currency\",\n    currency,\n    minimumFractionDigits: 0,\n    maximumFractionDigits: 0,\n  });\n\n  const formatted = formatter.format(amount);\n  return period === \"monthly\" ? `${formatted}/mo` : `${formatted}/yr`;\n}\n\nexport default function BillingPlanSelector({\n  plans,\n  selectedPlanId,\n  billingPeriod = \"monthly\",\n  onBillingPeriodChange,\n  onPlanSelect,\n  className,\n  showAnnualSavings = true,\n  currency = \"USD\",\n  layout = \"grid\",\n}: BillingPlanSelectorProps) {\n  const [isLoading, setIsLoading] = useState<string | null>(null);\n\n  const handlePlanSelect = async (planId: string) => {\n    setIsLoading(planId);\n    try {\n      await onPlanSelect?.(planId);\n    } finally {\n      setIsLoading(null);\n    }\n  };\n\n  const calculateSavings = (plan: SelectablePlan): number | null => {\n    if (!showAnnualSavings || billingPeriod === \"annual\") return null;\n    const monthlyTotal = plan.price.monthly * 12;\n    const annualTotal = plan.price.annual;\n    const savings = monthlyTotal - annualTotal;\n    return savings > 0 ? savings : null;\n  };\n\n  const defaultCurrency = currency || plans[0]?.currency || \"USD\";\n\n  return (\n    <div className={cn(\"flex w-full flex-col gap-6\", className)}>\n      {plans.length > 0 && (\n        <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n          <div className=\"flex flex-col gap-1\">\n            <h2 className=\"font-semibold text-lg\">Choose your plan</h2>\n            <p className=\"text-muted-foreground text-sm\">\n              Select the plan that best fits your needs\n            </p>\n          </div>\n          <div className=\"flex items-center gap-3\">\n            <span\n              className={cn(\n                \"text-sm\",\n                billingPeriod === \"monthly\" && \"font-medium\"\n              )}\n            >\n              Monthly\n            </span>\n            <Switch\n              checked={billingPeriod === \"annual\"}\n              onCheckedChange={(checked) =>\n                onBillingPeriodChange?.(checked ? \"annual\" : \"monthly\")\n              }\n            />\n            <div className=\"flex flex-col\">\n              <span\n                className={cn(\n                  \"text-sm\",\n                  billingPeriod === \"annual\" && \"font-medium\"\n                )}\n              >\n                Annual\n              </span>\n              {showAnnualSavings && billingPeriod === \"annual\" && (\n                <span className=\"text-primary text-xs\">Save up to 20%</span>\n              )}\n            </div>\n          </div>\n        </div>\n      )}\n\n      <div\n        className={cn(\n          \"grid gap-4\",\n          layout === \"grid\"\n            ? \"grid-cols-1 md:grid-cols-2 lg:grid-cols-3\"\n            : \"grid-cols-1\"\n        )}\n      >\n        {plans.map((plan) => {\n          const savings = calculateSavings(plan);\n          const price =\n            billingPeriod === \"monthly\"\n              ? plan.price.monthly\n              : plan.price.annual;\n          const isSelected = selectedPlanId === plan.id;\n\n          return (\n            <Card\n              className={cn(\n                \"relative flex flex-col shadow-xs transition-all\",\n                plan.isCurrent && \"border-primary\",\n                plan.isPopular && \"border-primary shadow-md\",\n                isSelected && \"ring-2 ring-primary\",\n                plan.disabled && \"opacity-50\"\n              )}\n              key={plan.id}\n            >\n              {plan.isPopular && (\n                <div className=\"absolute -top-3 left-1/2 -translate-x-1/2\">\n                  <Badge className=\"bg-primary text-primary-foreground\">\n                    Popular\n                  </Badge>\n                </div>\n              )}\n              <CardHeader>\n                <div className=\"flex flex-col gap-2\">\n                  <div className=\"flex items-center justify-between\">\n                    <CardTitle className=\"wrap-break-word\">\n                      {plan.name}\n                    </CardTitle>\n                    {plan.isCurrent && (\n                      <Badge variant=\"secondary\">Current</Badge>\n                    )}\n                  </div>\n                  {plan.description && (\n                    <CardDescription className=\"wrap-break-word\">\n                      {plan.description}\n                    </CardDescription>\n                  )}\n                  <div className=\"flex items-baseline gap-1\">\n                    <span className=\"font-semibold text-3xl\">\n                      {\n                        formatPrice(\n                          price,\n                          plan.currency || defaultCurrency,\n                          billingPeriod\n                        ).split(\"/\")[0]\n                      }\n                    </span>\n                    <span className=\"text-muted-foreground text-sm\">\n                      /{billingPeriod === \"monthly\" ? \"mo\" : \"yr\"}\n                    </span>\n                  </div>\n                  {savings && (\n                    <p className=\"text-primary text-sm\">\n                      Save{\" \"}\n                      {\n                        formatPrice(\n                          savings,\n                          plan.currency || defaultCurrency,\n                          \"annual\"\n                        ).split(\"/\")[0]\n                      }{\" \"}\n                      per year\n                    </p>\n                  )}\n                </div>\n              </CardHeader>\n              <CardContent>\n                <div className=\"flex flex-1 flex-col gap-6\">\n                  {plan.features.length > 0 && (\n                    <div className=\"flex flex-col gap-2\">\n                      {plan.features.map((feature, idx) => (\n                        <div className=\"flex items-start gap-2\" key={idx}>\n                          {feature.included !== false ? (\n                            <Check className=\"size-4 shrink-0 text-primary\" />\n                          ) : (\n                            <div className=\"size-4 shrink-0\" />\n                          )}\n                          <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n                            <span className=\"wrap-break-word text-sm\">\n                              {feature.name}\n                              {feature.limit !== undefined &&\n                                feature.included !== false && (\n                                  <span className=\"text-muted-foreground\">\n                                    {\" \"}\n                                    ({feature.limit})\n                                  </span>\n                                )}\n                            </span>\n                          </div>\n                        </div>\n                      ))}\n                    </div>\n                  )}\n                  <Button\n                    aria-busy={isLoading === plan.id}\n                    className=\"w-full\"\n                    data-loading={isLoading === plan.id}\n                    disabled={\n                      isLoading === plan.id || plan.disabled || plan.isCurrent\n                    }\n                    onClick={() => handlePlanSelect(plan.id)}\n                    type=\"button\"\n                    variant={\n                      plan.isCurrent\n                        ? \"outline\"\n                        : isSelected\n                          ? \"default\"\n                          : \"default\"\n                    }\n                  >\n                    {isLoading === plan.id ? (\n                      <>\n                        <Loader2 className=\"size-4 animate-spin\" />\n                        Processing…\n                      </>\n                    ) : plan.isCurrent ? (\n                      \"Current plan\"\n                    ) : (\n                      plan.ctaLabel || \"Select plan\"\n                    )}\n                  </Button>\n                </div>\n              </CardContent>\n            </Card>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "billing"
  ],
  "type": "registry:ui"
}