pricing.tsx
"use client"
import { useState } from "react"
import { AnimatePresence, motion, type Variants } from "motion/react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { FancyButton } from "@/components/ui/fancy-button"
import { ArrowUpRightIcon, CheckIcon } from 'lucide-react'
const animVariant: Variants = {
hidden: { opacity: 0, scale: 0.3, y: -20 },
visible: (i: number) => ({
opacity: 1,
scale: 1,
y: 0,
transition: {
delay: i * 0.05,
type: "spring",
damping: 12,
stiffness: 300,
},
}),
exit: {
opacity: 0,
scale: 0.3,
y: 20,
transition: { duration: 0.2 },
},
}
type PricingPlan = {
label: string
badge?: string
description: string
monthlyPrice: number
yearlyPrice: number
features: string[]
highlighted?: boolean
ctaText: string
}
const plans: PricingPlan[] = [
{
label: "Starter",
description: "For solo developers and small teams getting started",
monthlyPrice: 29,
yearlyPrice: 24,
features: [
"Up to 5 projects",
"5GB storage",
"Basic analytics",
"Email support",
"Community access",
"Weekly deploy reports",
"API access",
"CLI & mobile app",
],
ctaText: "Start free trial",
},
{
label: "Pro",
badge: "Most popular",
description: "For teams shipping production apps at scale",
highlighted: true,
monthlyPrice: 99,
yearlyPrice: 79,
features: [
"Unlimited projects",
"500GB storage",
"Advanced analytics",
"Priority support",
"Team management",
"Custom CI workflows",
"Advanced API",
"Integrations",
],
ctaText: "Get started now",
},
]
export function Pricing() {
const [billingPeriod, setBillingPeriod] = useState<"monthly" | "yearly">(
"monthly"
)
return (
<section aria-label="Pricing" className="mx-auto w-full max-w-7xl">
<div className="flex flex-col items-center gap-8">
<div className="flex flex-col items-center gap-4 text-center">
<Badge variant="secondary" className="w-fit">
Simple and transparent pricing
</Badge>
<h2 className="text-4xl font-bold tracking-tight text-balance text-foreground sm:text-5xl">
Plans for every scale
</h2>
<p className="max-w-2xl text-lg text-pretty text-muted-foreground">
Start free, grow fast. Choose the plan that fits your projects, with
no hidden fees.
</p>
</div>
<Tabs
value={billingPeriod}
onValueChange={(value) =>
setBillingPeriod(value as "monthly" | "yearly")
}
>
<TabsList>
<TabsTrigger value="monthly">Monthly</TabsTrigger>
<TabsTrigger value="yearly" className="gap-1.5">
Yearly
<Badge
variant="secondary"
className="h-5 bg-primary/10 text-xs whitespace-nowrap text-primary hover:bg-primary/10"
>
Save 20%
</Badge>
</TabsTrigger>
</TabsList>
</Tabs>
<div className="grid w-full grid-cols-1 gap-6 lg:grid-cols-2">
{plans.map((plan) => (
<PricingCard
key={plan.label}
plan={plan}
billingPeriod={billingPeriod}
/>
))}
</div>
</div>
</section>
)
}
function PricingCard({
plan,
billingPeriod,
}: {
plan: PricingPlan
billingPeriod: "monthly" | "yearly"
}) {
const price =
billingPeriod === "monthly" ? plan.monthlyPrice : plan.yearlyPrice
const priceChars = price.toString().split("")
return (
<div className="rounded-4xl bg-muted p-2">
<div className="grid h-full grid-cols-1 gap-2 md:grid-cols-[1fr_1.25fr]">
<div className="flex flex-col gap-8 p-8">
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<h3 className="text-2xl font-semibold text-balance text-foreground">
{plan.label}
</h3>
{plan.badge && (
<Badge
variant="outline"
className="border-dashed border-primary/30 whitespace-nowrap text-primary"
>
{plan.badge}
</Badge>
)}
</div>
<p className="text-sm leading-relaxed text-pretty text-muted-foreground">
{plan.description}
</p>
</div>
<div className="flex flex-col gap-1">
<div className="flex items-baseline gap-1">
<span className="text-6xl font-semibold tracking-tight text-foreground">
$
</span>
<div className="flex text-6xl font-semibold tracking-tight text-foreground tabular-nums">
<AnimatePresence mode="popLayout">
{priceChars.map((char, idx) => (
<motion.span
key={`${billingPeriod}-${price}-${idx}`}
variants={animVariant}
initial="hidden"
animate="visible"
exit="exit"
custom={idx}
>
{char}
</motion.span>
))}
</AnimatePresence>
</div>
<span className="text-lg text-muted-foreground">/month</span>
</div>
<p className="text-sm text-muted-foreground">billed annually</p>
</div>
{plan.highlighted ? (
<FancyButton size="lg" className="w-full">
{plan.ctaText}
<ArrowUpRightIcon />
</FancyButton>
) : (
<Button variant="outline" size="lg" className="w-full">
{plan.ctaText}
<ArrowUpRightIcon />
</Button>
)}
</div>
<div className="flex flex-col gap-4 rounded-3xl bg-card p-8 shadow-elevated-lg">
<ul className="flex flex-col gap-4">
{plan.features.map((feature) => (
<li key={feature} className="flex items-start gap-3">
<div className="mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full bg-primary">
<CheckIcon className="size-2.5 text-primary-foreground" />
</div>
<span className="text-sm text-muted-foreground">{feature}</span>
</li>
))}
</ul>
</div>
</div>
</div>
)
}