{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "billing-pricing-table",
  "title": "Billing Pricing Table",
  "description": "Compare pricing plans in a table format with feature comparison.",
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "switch",
    "tooltip"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/billing/billing-pricing-table.tsx",
      "content": "\"use client\";\n\nimport { Check, HelpCircle, Loader2, X } 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\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/registry/new-york/ui/tooltip\";\n\nexport interface PricingPlan {\n  id: string;\n  name: string;\n  description?: string;\n  price: {\n    monthly: number;\n    annual: number;\n  };\n  currency?: string;\n  isPopular?: boolean;\n  isCurrent?: boolean;\n  features: PricingFeature[];\n  ctaLabel?: string;\n  ctaVariant?: \"default\" | \"outline\";\n}\n\nexport interface PricingFeature {\n  name: string;\n  description?: string;\n  values: {\n    [planId: string]: string | boolean | number | null;\n  };\n  tooltip?: string;\n}\n\nexport interface BillingPricingTableProps {\n  plans: PricingPlan[];\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  mobileView?: \"table\" | \"cards\";\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\nfunction formatFeatureValue(\n  value: string | boolean | number | null\n): React.ReactNode {\n  if (value === null || value === false) {\n    return <X className=\"size-4 text-muted-foreground\" />;\n  }\n  if (value === true) {\n    return <Check className=\"size-4 text-primary\" />;\n  }\n  if (typeof value === \"string\") {\n    return <span className=\"text-sm\">{value}</span>;\n  }\n  return <span className=\"text-sm\">{value}</span>;\n}\n\nexport default function BillingPricingTable({\n  plans,\n  billingPeriod = \"monthly\",\n  onBillingPeriodChange,\n  onPlanSelect,\n  className,\n  showAnnualSavings = true,\n  currency = \"USD\",\n  mobileView = \"cards\",\n}: BillingPricingTableProps) {\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: PricingPlan): 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 allFeatures = plans[0]?.features || [];\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              Compare features and pricing\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      {mobileView === \"cards\" ? (\n        <div className=\"flex flex-col gap-4 md:hidden\">\n          {plans.map((plan) => {\n            const savings = calculateSavings(plan);\n            const price =\n              billingPeriod === \"monthly\"\n                ? plan.price.monthly\n                : plan.price.annual;\n\n            return (\n              <Card\n                className={cn(\n                  \"relative w-full shadow-xs\",\n                  plan.isCurrent && \"border-primary\",\n                  plan.isPopular && \"border-primary shadow-md\"\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-col gap-6\">\n                    <div className=\"flex flex-col gap-3\">\n                      {allFeatures.map((feature, idx) => {\n                        const value = feature.values[plan.id];\n                        const hasValue = value !== null && value !== false;\n\n                        let featureTitle = feature.name;\n                        if (hasValue) {\n                          if (\n                            typeof value === \"string\" ||\n                            typeof value === \"number\"\n                          ) {\n                            featureTitle = `${value} ${feature.name}`;\n                          } else if (value === true) {\n                            featureTitle = feature.name;\n                          }\n                        }\n\n                        return (\n                          <div className=\"flex items-start gap-3\" key={idx}>\n                            {hasValue && (\n                              <div className=\"shrink-0\">\n                                <Check className=\"size-4 text-primary\" />\n                              </div>\n                            )}\n                            <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n                              <div className=\"flex items-center gap-2\">\n                                <span className=\"wrap-break-word font-medium text-sm\">\n                                  {featureTitle}\n                                </span>\n                                {feature.tooltip && (\n                                  <Tooltip>\n                                    <TooltipTrigger asChild>\n                                      <HelpCircle className=\"size-3.5 shrink-0 text-muted-foreground\" />\n                                    </TooltipTrigger>\n                                    <TooltipContent className=\"max-w-xs\">\n                                      <p className=\"wrap-break-word\">\n                                        {feature.tooltip}\n                                      </p>\n                                    </TooltipContent>\n                                  </Tooltip>\n                                )}\n                              </div>\n                              {feature.description && (\n                                <p className=\"wrap-break-word text-muted-foreground text-xs\">\n                                  {feature.description}\n                                </p>\n                              )}\n                            </div>\n                          </div>\n                        );\n                      })}\n                    </div>\n                    <Button\n                      aria-busy={isLoading === plan.id}\n                      className=\"w-full\"\n                      data-loading={isLoading === plan.id}\n                      disabled={isLoading === plan.id || plan.isCurrent}\n                      onClick={() => handlePlanSelect(plan.id)}\n                      type=\"button\"\n                      variant={\n                        plan.isCurrent\n                          ? \"outline\"\n                          : plan.ctaVariant || \"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      ) : null}\n\n      <div className=\"hidden w-full overflow-x-auto md:block\">\n        <table className=\"w-full border-collapse\">\n          <thead>\n            <tr>\n              <th className=\"sticky left-0 z-10 bg-background p-4 text-left\">\n                <span className=\"font-medium text-sm\">Features</span>\n              </th>\n              {plans.map((plan) => {\n                const savings = calculateSavings(plan);\n                const price =\n                  billingPeriod === \"monthly\"\n                    ? plan.price.monthly\n                    : plan.price.annual;\n\n                return (\n                  <th\n                    className={cn(\n                      \"relative p-4 text-center\",\n                      plan.isCurrent && \"bg-primary/5\",\n                      plan.isPopular && \"bg-primary/10\"\n                    )}\n                    key={plan.id}\n                  >\n                    <div className=\"flex flex-col gap-2\">\n                      {plan.isPopular && (\n                        <Badge className=\"mx-auto bg-primary text-primary-foreground\">\n                          Popular\n                        </Badge>\n                      )}\n                      <div className=\"flex flex-col gap-1\">\n                        <CardTitle className=\"wrap-break-word\">\n                          {plan.name}\n                        </CardTitle>\n                        {plan.description && (\n                          <CardDescription className=\"wrap-break-word text-xs\">\n                            {plan.description}\n                          </CardDescription>\n                        )}\n                      </div>\n                      <div className=\"flex items-baseline justify-center 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-xs\">\n                          Save{\" \"}\n                          {\n                            formatPrice(\n                              savings,\n                              plan.currency || defaultCurrency,\n                              \"annual\"\n                            ).split(\"/\")[0]\n                          }\n                          /yr\n                        </p>\n                      )}\n                      {plan.isCurrent && (\n                        <Badge className=\"mx-auto\" variant=\"secondary\">\n                          Current\n                        </Badge>\n                      )}\n                    </div>\n                  </th>\n                );\n              })}\n            </tr>\n          </thead>\n          <tbody>\n            {allFeatures.map((feature, idx) => (\n              <tr className=\"border-b last:border-b-0\" key={idx}>\n                <td className=\"sticky left-0 z-10 bg-background p-4\">\n                  <div className=\"flex flex-col gap-1\">\n                    <div className=\"flex items-center gap-2\">\n                      <span className=\"wrap-break-word font-medium text-sm\">\n                        {feature.name}\n                      </span>\n                      {feature.tooltip && (\n                        <Tooltip>\n                          <TooltipTrigger asChild>\n                            <HelpCircle className=\"size-3.5 shrink-0 text-muted-foreground\" />\n                          </TooltipTrigger>\n                          <TooltipContent className=\"max-w-xs\">\n                            <p className=\"wrap-break-word\">{feature.tooltip}</p>\n                          </TooltipContent>\n                        </Tooltip>\n                      )}\n                    </div>\n                    {feature.description && (\n                      <p className=\"wrap-break-word text-muted-foreground text-xs\">\n                        {feature.description}\n                      </p>\n                    )}\n                  </div>\n                </td>\n                {plans.map((plan) => {\n                  const value = feature.values[plan.id];\n                  return (\n                    <td\n                      className={cn(\n                        \"p-4 text-center\",\n                        plan.isCurrent && \"bg-primary/5\"\n                      )}\n                      key={plan.id}\n                    >\n                      <div className=\"flex items-center justify-center\">\n                        {formatFeatureValue(value)}\n                      </div>\n                    </td>\n                  );\n                })}\n              </tr>\n            ))}\n          </tbody>\n          <tfoot>\n            <tr>\n              <td className=\"sticky left-0 z-10 bg-background p-4\" />\n              {plans.map((plan) => (\n                <td className=\"p-4\" key={plan.id}>\n                  <Button\n                    aria-busy={isLoading === plan.id}\n                    className=\"w-full\"\n                    data-loading={isLoading === plan.id}\n                    disabled={isLoading === plan.id || plan.isCurrent}\n                    onClick={() => handlePlanSelect(plan.id)}\n                    type=\"button\"\n                    variant={\n                      plan.isCurrent ? \"outline\" : plan.ctaVariant || \"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                </td>\n              ))}\n            </tr>\n          </tfoot>\n        </table>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "billing"
  ],
  "type": "registry:ui"
}