{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "staged-pricing", "type": "registry:ui", "title": "Staged Pricing", "description": "Four-tier pricing section framed by decorative grid lines and corner crosshairs, with an animated monthly/annual toggle and a slide-up price swap.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "button" ], "files": [ { "path": "registry/ruixenui/staged-pricing.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport Link from \"next/link\";\nimport { Check } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\n/* ── types ───────────────────────────────────────────────────── */\n\ntype BillingCycle = \"monthly\" | \"annual\";\n\nexport interface StagedPricingPlan {\n name: string;\n audience: string;\n features: string[];\n /** Monthly price. Number renders with `$` prefix, string renders verbatim (e.g. \"Custom\"). */\n monthly: number | string;\n /** Annual price (per user / month). Falls back to `monthly` if omitted. */\n annual?: number | string;\n /** Renders a \"Save N%\" badge next to the price when annual is active. */\n annualSavingsPct?: number;\n cta: { label: string; href?: string };\n /** Highlights this card with a primary ring + primary CTA. */\n highlighted?: boolean;\n /** Override the caption under the price line. */\n priceCaption?: string;\n}\n\nexport interface StagedPricingProps {\n title?: React.ReactNode;\n description?: React.ReactNode;\n plans: StagedPricingPlan[];\n defaultCycle?: BillingCycle;\n hideToggle?: boolean;\n cycleLabels?: { monthly: string; annual: string };\n className?: string;\n}\n\n/* ── helpers ─────────────────────────────────────────────────── */\n\nconst gridColsByCount: Record = {\n 1: \"lg:grid-cols-1\",\n 2: \"lg:grid-cols-2\",\n 3: \"lg:grid-cols-3\",\n 4: \"lg:grid-cols-4\",\n};\n\n/* ── toggle ──────────────────────────────────────────────────── */\n\nfunction CycleToggle({\n cycle,\n onChange,\n labels,\n}: {\n cycle: BillingCycle;\n onChange: (c: BillingCycle) => void;\n labels: { monthly: string; annual: string };\n}) {\n return (\n
\n
\n \n {([\"monthly\", \"annual\"] as const).map((c) => (\n onChange(c)}\n aria-pressed={cycle === c}\n className={cn(\n \"isolate cursor-pointer rounded-[10px] px-5 py-2 text-sm transition-colors duration-500\",\n cycle === c ? \"text-foreground\" : \"text-muted-foreground\",\n )}\n >\n {labels[c]}\n \n ))}\n
\n
\n );\n}\n\n/* ── animated price ──────────────────────────────────────────── */\n\nfunction AnimatedPrice({\n plan,\n cycle,\n}: {\n plan: StagedPricingPlan;\n cycle: BillingCycle;\n}) {\n const annual = plan.annual ?? plan.monthly;\n const monthly = plan.monthly;\n const annualIsNumber = typeof annual === \"number\";\n const monthlyIsNumber = typeof monthly === \"number\";\n const showDollar = annualIsNumber || monthlyIsNumber;\n\n // Identical static value (e.g. \"Custom\"): no animation.\n if (!annualIsNumber && !monthlyIsNumber && annual === monthly) {\n return (\n \n {monthly}\n \n );\n }\n\n return (\n \n {showDollar && $}\n \n \n {annual}\n \n \n {monthly}\n \n \n \n );\n}\n\n/* ── decorative frame ────────────────────────────────────────── */\n\nfunction Crosshair({ className }: { className?: string }) {\n return (\n