"use client"; import { CheckIcon, ChevronDownIcon, MinusIcon, type LucideIcon } from "lucide-react"; import { motion, AnimatePresence, useReducedMotion } from "motion/react"; import { type KeyboardEvent, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "@/components/ui/collapsible"; import { cn } from "@/lib/utils"; export type Billing = "monthly" | "yearly"; export interface PricingPlan { id: string; name: string; description: string; monthly: number; yearly: number; cta: string; icon?: LucideIcon; highlights: string[]; featured?: boolean; badge?: string; } export interface PricingFeatureGroup { name: string; rows: { label: string; values: Record }[]; } export interface PricingTableProps { plans: PricingPlan[]; features?: PricingFeatureGroup[]; eyebrow?: string; title?: string; subtitle?: string; currency?: string; yearlyBadge?: string; defaultBilling?: Billing; onSelect?: (plan: PricingPlan, billing: Billing) => void; className?: string; } const springy = { type: "spring", stiffness: 420, damping: 40 } as const; const glide = { type: "spring", stiffness: 520, damping: 44 } as const; const press = "transition-[box-shadow,background-color,transform] duration-150 ease-out active:scale-[0.97] motion-reduce:active:scale-100"; function RollingDigit({ digit }: { digit: number }) { const reduce = useReducedMotion(); return ( {Array.from({ length: 10 }, (_, n) => ( {n} ))} ); } function RollingNumber({ value, className }: { value: number; className?: string }) { const text = value.toLocaleString("en-US"); const chars = text.split(""); return ( {text} {chars.map((char, i) => { const key = chars.length - i; return /\d/.test(char) ? ( ) : ( {char} ); })} ); } function BillingSwitch({ value, onChange, yearlyBadge, }: { value: Billing; onChange: (value: Billing) => void; yearlyBadge?: string; }) { const reduce = useReducedMotion(); const refs = useRef<(HTMLButtonElement | null)[]>([]); const options: { value: Billing; label: string }[] = [ { value: "monthly", label: "Monthly" }, { value: "yearly", label: "Yearly" }, ]; const index = options.findIndex((o) => o.value === value); const onKeyDown = (event: KeyboardEvent) => { if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) return; event.preventDefault(); const next = index === 0 ? 1 : 0; onChange(options[next].value); refs.current[next]?.focus(); }; return (
{options.map((option, i) => { const checked = option.value === value; return ( ); })}
); } function PlanCard({ plan, billing, currency, featured, onSelect, }: { plan: PricingPlan; billing: Billing; currency: string; featured: boolean; onSelect?: (plan: PricingPlan, billing: Billing) => void; }) { const reduce = useReducedMotion(); const price = billing === "yearly" ? plan.yearly : plan.monthly; const saving = plan.monthly - plan.yearly; const Icon = plan.icon; return (
{featured && ( <> )}
{Icon && ( )}

{plan.name}

{(plan.badge || featured) && ( {plan.badge ?? "Most popular"} )}

{plan.description}

{currency} /mo {billing === "yearly" && saving > 0 && ( {currency} {plan.monthly} )}
{billing === "yearly" ? `${currency}${(plan.yearly * 12).toLocaleString("en-US")} billed yearly` : "Billed monthly, cancel anytime"}
    {plan.highlights.map((item) => (
  • {item}
  • ))}
); } function FeatureValue({ value }: { value: boolean | string | undefined }) { if (value === true) { return ( ); } if (!value) return ; return {value}; } export function PricingTable({ plans, features, eyebrow = "Pricing", title = "Simple plans that grow with you", subtitle = "Start free for 14 days. Switch or cancel whenever you like.", currency = "$", yearlyBadge = "−20%", defaultBilling = "monthly", onSelect, className, }: PricingTableProps) { const [billing, setBilling] = useState(defaultBilling); const featuredIndex = plans.some((p) => p.featured) ? plans.findIndex((p) => p.featured) : Math.floor((plans.length - 1) / 2); return (
{eyebrow &&

{eyebrow}

}

{title}

{subtitle &&

{subtitle}

}
{plans.map((plan, i) => ( ))}
{features && features.length > 0 && (
}> Compare all features
{plans.map((plan, i) => ( ))} {features.map((group) => ( {group.rows.map((row) => ( {plans.map((plan, i) => ( ))} ))} ))}
Features {plan.name}
{group.name}
{row.label}
)}
); }