pricing.tsx
"use client"
import { useId, useState } from "react"
import {
AnimatePresence,
motion,
useReducedMotion,
type Variants,
} from "motion/react"
import { Badge } from "@/components/ui/badge"
import { Slider } from "@/components/ui/slider"
import { FancyButton } from "@/components/ui/fancy-button"
import { ArrowUpRightIcon, CheckIcon } from 'lucide-react'
const animVariant: Variants = {
hidden: { opacity: 0, y: -20, scale: 1.15 },
visible: (i: number) => ({
opacity: 1,
y: 0,
scale: 1,
transition: {
delay: i * 0.04,
type: "spring",
damping: 16,
stiffness: 380,
},
}),
exit: {
opacity: 0,
y: -12,
scale: 1.1,
transition: { duration: 0.15, ease: "easeIn" },
},
}
type PriceTier = {
maxVolume: number
price: number
}
const priceTiers: PriceTier[] = [
{ maxVolume: 1000, price: 29 },
{ maxVolume: 3000, price: 79 },
{ maxVolume: 5000, price: 149 },
{ maxVolume: 7500, price: 249 },
{ maxVolume: 10000, price: 399 },
]
const features = [
"Unlimited team members",
"Volume-based pricing",
"Advanced automation workflows",
"Real-time analytics dashboard",
"Dedicated success manager",
]
export function Pricing() {
const [emailVolume, setEmailVolume] = useState(5000)
const price = (
priceTiers.find((tier) => emailVolume <= tier.maxVolume) ??
priceTiers[priceTiers.length - 1]!
).price
return (
<section aria-label="Pricing" className="mx-auto w-full max-w-5xl">
<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">
Flexible Pricing
</Badge>
<h2 className="text-4xl font-bold tracking-tight text-foreground sm:text-5xl">
Pay for what you use
</h2>
<p className="max-w-xl text-base text-muted-foreground">
Simple, transparent pricing that scales with your email volume. No
hidden fees, no surprises.
</p>
</div>
<div className="w-full rounded-4xl bg-muted p-1">
<div className="grid grid-cols-1 gap-2 lg:grid-cols-2">
<VolumeCard
emailVolume={emailVolume}
setEmailVolume={setEmailVolume}
/>
<PlanCard price={price} />
</div>
</div>
</div>
</section>
)
}
function VolumeCard({
emailVolume,
setEmailVolume,
}: {
emailVolume: number
setEmailVolume: (value: number) => void
}) {
const volumeLabelId = useId()
const conversions = Math.round(emailVolume * 0.15)
const revenue = Math.round(emailVolume * 0.15 * 45)
return (
<div className="flex flex-col gap-6 rounded-3xl bg-card p-8 shadow-elevated-lg">
<Badge variant="secondary" className="w-fit gap-1.5">
<div className="size-2 rounded-full bg-primary" />
Usage-based pricing
</Badge>
<div className="flex flex-col gap-2">
<h3
id={volumeLabelId}
className="text-2xl font-semibold text-foreground"
>
Monthly email volume?
</h3>
<p className="text-sm text-muted-foreground">
Flexible pricing that grows with your subscriber base. Pay only for
emails that deliver results.
</p>
</div>
<div className="flex flex-col gap-3">
<p className="text-sm font-medium text-foreground">
{emailVolume.toLocaleString()} emails/month
</p>
<Slider
aria-labelledby={volumeLabelId}
value={[emailVolume]}
onValueChange={(value) =>
setEmailVolume(Array.isArray(value) ? (value[0] ?? 0) : value)
}
min={0}
max={10000}
step={100}
className="cursor-grab active:cursor-grabbing **:data-[slot=slider-thumb]:h-6.5 **:data-[slot=slider-thumb]:w-3 **:data-[slot=slider-thumb]:border-3 **:data-[slot=slider-thumb]:border-card **:data-[slot=slider-thumb]:bg-primary **:data-[slot=slider-thumb]:ring-0 **:data-[slot=slider-thumb]:focus-within:ring-3 **:data-[slot=slider-track]:h-2 **:data-[slot=slider-track]:bg-input/60"
/>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>0</span>
<span>10,000</span>
</div>
</div>
<div className="flex flex-col gap-3">
<h3 className="text-lg font-semibold text-foreground">
Expected results
</h3>
<p className="text-sm text-muted-foreground">
With {emailVolume.toLocaleString()} emails per month, here's the
typical performance our customers achieve.
</p>
<ul className="flex flex-col gap-3">
<li 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">
Est. conversions: {conversions.toLocaleString()}
</span>
</li>
<li 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">
Revenue impact: ${revenue.toLocaleString()}
</span>
</li>
</ul>
</div>
</div>
)
}
function PlanCard({ price }: { price: number }) {
const shouldReduceMotion = useReducedMotion()
const priceChars = String(price).split("")
return (
<div className="flex flex-col gap-6 p-8">
<div className="flex flex-col gap-2">
<h3 className="text-2xl font-semibold text-foreground">
Professional plan
</h3>
<p className="text-sm leading-relaxed text-muted-foreground">
For growing teams managing email campaigns, automation, and customer
engagement at scale.
</p>
</div>
<div className="flex items-baseline gap-1">
<div className="flex text-6xl font-semibold tracking-tight text-foreground">
<span>$</span>
<AnimatePresence initial={false} mode="popLayout">
{priceChars.map((char, idx) => (
<motion.span
key={`${price}-${idx}`}
variants={animVariant}
initial={shouldReduceMotion ? false : "hidden"}
animate="visible"
exit={shouldReduceMotion ? undefined : "exit"}
custom={idx}
>
{char}
</motion.span>
))}
</AnimatePresence>
</div>
<span className="text-xl text-muted-foreground">/mo</span>
</div>
<ul className="flex flex-col gap-3">
{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>
<FancyButton size="lg" className="w-full">
Start free trial
<ArrowUpRightIcon data-icon="inline-end" />
</FancyButton>
</div>
)
}