{
  "$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<number, string> = {\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    <div className=\"rounded-xl bg-muted p-0.5\">\n      <div className=\"relative grid grid-cols-2 gap-x-0.5\">\n        <div\n          aria-hidden\n          className={cn(\n            \"absolute left-0 top-0 h-full w-[calc((100%-2px)/2)] rounded-[10px] bg-background shadow-sm ring-1 ring-border/50 transition-transform duration-500 ease-out\",\n            cycle === \"annual\" && \"translate-x-[calc(100%+2px)]\",\n          )}\n        />\n        {([\"monthly\", \"annual\"] as const).map((c) => (\n          <button\n            key={c}\n            type=\"button\"\n            onClick={() => 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          </button>\n        ))}\n      </div>\n    </div>\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      <span className=\"text-5xl font-semibold tracking-tight text-foreground\">\n        {monthly}\n      </span>\n    );\n  }\n\n  return (\n    <span className=\"inline-flex items-start text-5xl font-semibold tracking-tight text-foreground\">\n      {showDollar && <span>$</span>}\n      <span className=\"inline-grid overflow-y-hidden\">\n        <span\n          aria-hidden={cycle !== \"annual\"}\n          className={cn(\n            \"col-start-1 row-start-1 transition duration-500 ease-out\",\n            cycle === \"annual\"\n              ? \"translate-y-0 opacity-100\"\n              : \"translate-y-full opacity-0\",\n          )}\n        >\n          {annual}\n        </span>\n        <span\n          aria-hidden={cycle !== \"monthly\"}\n          className={cn(\n            \"col-start-1 row-start-1 transition duration-500 ease-out\",\n            cycle === \"monthly\"\n              ? \"translate-y-0 opacity-100\"\n              : \"-translate-y-full opacity-0\",\n          )}\n        >\n          {monthly}\n        </span>\n      </span>\n    </span>\n  );\n}\n\n/* ── decorative frame ────────────────────────────────────────── */\n\nfunction Crosshair({ className }: { className?: string }) {\n  return (\n    <div\n      aria-hidden\n      className={cn(\"pointer-events-none absolute hidden lg:block\", className)}\n    >\n      <div className=\"relative h-[7px] w-[7px]\">\n        <div className=\"absolute left-0 top-[3px] h-px w-full rounded-full bg-foreground/40\" />\n        <div className=\"absolute left-[3px] top-0 h-full w-px rounded-full bg-foreground/40\" />\n      </div>\n    </div>\n  );\n}\n\nfunction GridFrameLines({ columns }: { columns: number }) {\n  return (\n    <>\n      {/* horizontal lines */}\n      <div\n        aria-hidden\n        className=\"pointer-events-none absolute -top-3 left-0 col-span-full hidden h-px w-full -translate-y-1/2 bg-[linear-gradient(to_right,_transparent_0%,_var(--color-border)_6.5%,_var(--color-border)_93.5%,_transparent_100%)] lg:block\"\n      />\n      <div\n        aria-hidden\n        className=\"pointer-events-none absolute -bottom-3 left-0 col-span-full hidden h-px w-full translate-y-1/2 bg-[linear-gradient(to_right,_transparent_0%,_var(--color-border)_6.5%,_var(--color-border)_93.5%,_transparent_100%)] lg:block\"\n      />\n\n      {/* vertical column-boundary lines (N+1 of them) */}\n      {Array.from({ length: columns + 1 }, (_, i) => {\n        const isLast = i === columns;\n        return (\n          <div\n            key={`v-${i}`}\n            aria-hidden\n            className={cn(\n              \"pointer-events-none absolute -top-12 -bottom-12 hidden w-px bg-[linear-gradient(to_bottom,_transparent_0%,_var(--color-border)_11%,_var(--color-border)_89%,_transparent_100%)] lg:block\",\n              isLast ? \"-right-3 translate-x-1/2\" : \"-left-3 -translate-x-1/2\",\n            )}\n            style={{ gridColumnStart: i + 1 }}\n          />\n        );\n      })}\n\n      {/* corner crosshairs */}\n      <Crosshair className=\"-left-3 -top-3 -translate-x-1/2 -translate-y-1/2\" />\n      <Crosshair className=\"-right-3 -top-3 translate-x-1/2 -translate-y-1/2\" />\n      <Crosshair className=\"-right-3 -bottom-3 translate-x-1/2 translate-y-1/2\" />\n      <Crosshair className=\"-left-3 -bottom-3 -translate-x-1/2 translate-y-1/2\" />\n    </>\n  );\n}\n\n/* ── card ────────────────────────────────────────────────────── */\n\nfunction PricingCard({\n  plan,\n  cycle,\n}: {\n  plan: StagedPricingPlan;\n  cycle: BillingCycle;\n}) {\n  const showSavings =\n    cycle === \"annual\" &&\n    typeof plan.annualSavingsPct === \"number\" &&\n    plan.annualSavingsPct > 0;\n\n  const ctaButton = (\n    <Button\n      asChild={Boolean(plan.cta.href)}\n      variant={plan.highlighted ? \"default\" : \"outline\"}\n      className=\"h-11 w-full rounded-xl text-base lg:h-9 lg:rounded-[10px] lg:text-sm\"\n    >\n      {plan.cta.href ? (\n        <Link href={plan.cta.href}>{plan.cta.label}</Link>\n      ) : (\n        <span>{plan.cta.label}</span>\n      )}\n    </Button>\n  );\n\n  return (\n    <div\n      className={cn(\n        \"relative flex flex-col justify-between rounded-3xl border bg-card px-[23px] pb-[23px] pt-[21px] shadow-sm\",\n        plan.highlighted\n          ? \"border-primary/40 ring-4 ring-primary/10\"\n          : \"border-border\",\n      )}\n    >\n      <div className=\"flex flex-col\">\n        <header className=\"text-xl text-muted-foreground\">{plan.name}</header>\n\n        <div className=\"mt-4 lg:mt-8\">\n          <div className=\"flex items-start gap-2\">\n            <AnimatedPrice plan={plan} cycle={cycle} />\n            {showSavings && (\n              <span\n                className={cn(\n                  \"mt-[6px] inline-flex items-center rounded-lg border border-primary/20 bg-primary/10 px-[7px] py-[3px] text-xs font-medium text-primary transition-opacity duration-300\",\n                  cycle === \"annual\" ? \"opacity-100\" : \"opacity-0\",\n                )}\n              >\n                Save {plan.annualSavingsPct}%\n              </span>\n            )}\n          </div>\n          {plan.priceCaption !== undefined ? (\n            <p className=\"mt-0.5 text-xs text-muted-foreground\">\n              {plan.priceCaption}\n            </p>\n          ) : (\n            <p className=\"mt-0.5 text-xs text-muted-foreground\">\n              Per user/month, billed{\" \"}\n              <span className=\"inline-grid\">\n                <span\n                  aria-hidden={cycle !== \"monthly\"}\n                  className={cn(\n                    \"col-start-1 row-start-1 transition-opacity duration-150\",\n                    cycle === \"monthly\" ? \"opacity-100\" : \"opacity-0\",\n                  )}\n                >\n                  monthly\n                </span>\n                <span\n                  aria-hidden={cycle !== \"annual\"}\n                  className={cn(\n                    \"col-start-1 row-start-1 transition-opacity duration-150\",\n                    cycle === \"annual\" ? \"opacity-100\" : \"opacity-0\",\n                  )}\n                >\n                  annually\n                </span>\n              </span>\n            </p>\n          )}\n        </div>\n\n        <div className=\"mt-5 text-pretty text-sm font-semibold text-foreground lg:mt-8\">\n          {plan.audience}\n        </div>\n\n        <ul className=\"mt-2.5 flex flex-col gap-y-2.5\">\n          {plan.features.map((feature) => (\n            <li key={feature} className=\"flex items-start gap-2\">\n              <span className=\"mt-px flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground\">\n                <Check className=\"size-3.5\" strokeWidth={2.5} />\n              </span>\n              <span className=\"text-pretty text-sm text-muted-foreground\">\n                {feature}\n              </span>\n            </li>\n          ))}\n        </ul>\n      </div>\n\n      <div className=\"mt-5 flex flex-col items-stretch lg:mt-8\">\n        {ctaButton}\n      </div>\n    </div>\n  );\n}\n\n/* ── component ───────────────────────────────────────────────── */\n\nexport function StagedPricing({\n  title = \"From zero to IPO.\",\n  description,\n  plans,\n  defaultCycle = \"annual\",\n  hideToggle = false,\n  cycleLabels = { monthly: \"Monthly\", annual: \"Annual\" },\n  className,\n}: StagedPricingProps) {\n  const [cycle, setCycle] = React.useState<BillingCycle>(defaultCycle);\n\n  const colsClass = gridColsByCount[plans.length] ?? \"lg:grid-cols-4\";\n\n  return (\n    <section className={cn(\"w-full bg-background py-20 lg:py-28\", className)}>\n      <div className=\"container\">\n        <div className=\"flex flex-col items-center\">\n          <h1 className=\"text-center text-4xl font-semibold tracking-tight text-foreground sm:text-5xl lg:text-6xl\">\n            {title}\n          </h1>\n          {description && (\n            <p className=\"mt-4 max-w-md text-balance text-center text-lg text-muted-foreground md:text-xl\">\n              {description}\n            </p>\n          )}\n        </div>\n\n        {!hideToggle && (\n          <div className=\"mt-8 flex flex-col items-center\">\n            <CycleToggle\n              cycle={cycle}\n              onChange={setCycle}\n              labels={cycleLabels}\n            />\n          </div>\n        )}\n\n        <div className=\"relative mt-10 lg:mt-20\">\n          <div\n            className={cn(\n              \"relative grid grid-cols-1 gap-6 xl:mx-auto xl:max-w-[83.333%]\",\n              colsClass,\n            )}\n          >\n            <GridFrameLines columns={plans.length} />\n            {plans.map((plan) => (\n              <PricingCard key={plan.name} plan={plan} cycle={cycle} />\n            ))}\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default StagedPricing;\n",
      "type": "registry:ui",
      "target": "components/ruixen/staged-pricing.tsx"
    }
  ]
}