"use client"; import { useEffect, useId, useState, type CSSProperties } from "react"; import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, useTransform } from "motion/react"; type Plan = { name: string; /** Price for one month. */ monthly: number; /** Price for the whole year. The saving badge is worked out from these two. */ yearly: number; /** Small line under the price. */ note?: string; features?: string[]; /** Marks this plan with the accent colour. */ featured?: boolean; }; type PricingSwitchProps = { plans: Plan[]; /** Start on the yearly side. */ defaultYearly?: boolean; /** Text on the two sides of the switch. */ labels?: [string, string]; /** Text after the price on each side. */ units?: [string, string]; /** Text before the price, like "$" or "₹". */ prefix?: string; /** Digits after the point. */ decimals?: number; /** Number format, like "en-IN" for lakh commas. */ locale?: string; /** Called when the switch moves. */ onChange?: (yearly: boolean) => void; /** Called when a plan button is pressed. Leave out to hide the buttons. */ onSelect?: (plan: Plan, yearly: boolean) => void; /** Text on the plan buttons. */ buttonLabel?: string; /** Switch, featured plan and tick colour. Text on it stays dark. */ accent?: string; /** Replaces the plan card look. */ className?: string; }; function Price({ value, format, reduce }: { value: number; format: (n: number) => string; reduce: boolean | null }) { const count = useMotionValue(value); const text = useTransform(count, format); useEffect(() => { if (reduce) { count.set(value); return; } const run = animate(count, value, { duration: 0.6, ease: [0.16, 1, 0.3, 1] }); return () => run.stop(); }, [count, reduce, value]); return ( <> {text} {format(value)} ); } /* PricingSwitch: a monthly and yearly switch whose pill slides across while every price counts over to the new number. */ export function PricingSwitch({ plans, defaultYearly = false, labels = ["Monthly", "Yearly"], units = ["/mo", "/yr"], prefix = "$", decimals = 0, locale = "en-US", onChange, onSelect, buttonLabel = "Choose plan", accent = "#a78bfa", className = "rounded-2xl border border-white/10 bg-white/5 p-5 text-white", }: PricingSwitchProps) { const reduce = useReducedMotion(); const id = useId(); const [yearly, setYearly] = useState(defaultYearly); const spring = reduce ? { duration: 0 } : { type: "spring" as const, stiffness: 380, damping: 32 }; const side = yearly ? 1 : 0; const format = (n: number) => `${prefix}${n.toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals })}`; const pick = (next: boolean) => { setYearly(next); onChange?.(next); }; return (
{labels.map((label, i) => ( ))}
{labels[side]} prices shown
{plans.map((plan) => { const saving = plan.monthly > 0 ? Math.round((1 - plan.yearly / (plan.monthly * 12)) * 100) : 0; return (

{plan.name}

{yearly && saving > 0 && ( Save {saving}% )}

{units[side]}

{plan.note &&

{plan.note}

} {plan.features && (
    {plan.features.map((feature) => (
  • {feature}
  • ))}
)} {onSelect && ( )}
); })}
); }