pricing.tsx
"use client"
import { useState } from "react"
import { cn } from "cn"
import { AnimatePresence, motion, type Variants } from "motion/react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch"
import { FancyButton } from "@/components/ui/fancy-button"
import { ArrowUpRightIcon, ChartLineIcon, CheckIcon, CodeIcon, DatabaseIcon, LockIcon, RepeatIcon, SettingsIcon } from 'lucide-react'
const animVariant: Variants = {
initial: { opacity: 0, y: 10, filter: "blur(4px)", scale: 0.98 },
animate: (i: number) => ({
opacity: 1,
y: 0,
filter: "blur(0px)",
scale: 1,
transition: {
delay: i * 0.03,
type: "spring",
damping: 22,
stiffness: 280,
},
}),
exit: {
opacity: 0,
y: -10,
filter: "blur(4px)",
scale: 0.98,
transition: { duration: 0.14 },
},
}
type FeatureToggle = {
id: string
label: string
icon?: React.ReactNode
price: number
enabled: boolean
}
const defaultFeatures: FeatureToggle[] = [
{
id: "analytics",
label: "Analytics engine",
icon: (
<ChartLineIcon className="size-4" />
),
price: 15,
enabled: true,
},
{
id: "api",
label: "API gateway",
icon: (
<CodeIcon className="size-4" />
),
price: 20,
enabled: true,
},
{
id: "storage",
label: "Cloud storage (100 GB)",
icon: (
<DatabaseIcon className="size-4" />
),
price: 10,
enabled: false,
},
{
id: "automation",
label: "Workflow automation",
icon: (
<RepeatIcon className="size-4" />
),
price: 25,
enabled: false,
},
{
id: "security",
label: "Advanced security",
icon: (
<LockIcon className="size-4" />
),
price: 30,
enabled: false,
},
{
id: "custom",
label: "Custom integrations",
icon: (
<SettingsIcon className="size-4" />
),
price: 20,
enabled: false,
},
]
const includedFeatures = [
"Unlimited team members",
"Core dashboard access",
"Email support",
"5 GB base storage",
]
const basePrice = 29
export function Pricing() {
const [features, setFeatures] = useState<FeatureToggle[]>(defaultFeatures)
const totalPrice =
basePrice +
features.filter((f) => f.enabled).reduce((sum, f) => sum + f.price, 0)
const priceChars = totalPrice.toString().split("")
const toggleFeature = (id: string) => {
setFeatures((prev) =>
prev.map((f) => (f.id === id ? { ...f, enabled: !f.enabled } : f))
)
}
return (
<section aria-label="Pricing" className="mx-auto w-full max-w-4xl">
<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">
Build your plan
</Badge>
<h2 className="text-4xl font-bold tracking-tight text-foreground sm:text-5xl">
Only pay for what you need
</h2>
<p className="max-w-2xl text-lg text-muted-foreground">
Start with a base plan and add modules as your infrastructure grows.
Full flexibility, zero waste.
</p>
</div>
<div className="grid w-full grid-cols-1 gap-4 lg:grid-cols-2">
<div className="flex flex-col gap-6 rounded-4xl bg-card p-8 shadow-elevated-lg">
<div className="flex flex-col gap-2">
<h3 className="text-lg font-medium text-foreground">
Add-on modules
</h3>
<p className="text-sm text-muted-foreground">
Toggle the features you need. Price updates in real-time.
</p>
</div>
<div className="flex flex-col gap-4">
{features.map((feature) => (
<div
key={feature.id}
className="flex items-center justify-between gap-4 rounded-lg p-2"
>
<Button
type="button"
variant="ghost"
onClick={() => toggleFeature(feature.id)}
className="h-auto flex-1 justify-start rounded-lg p-0 hover:bg-transparent"
>
<div className="flex items-center gap-3 text-left">
<div
className={cn(
"flex size-9 items-center justify-center rounded-full transition-colors",
feature.enabled
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground"
)}
>
{feature.icon}
</div>
<div className="flex flex-col">
<span className="text-sm font-medium text-foreground">
{feature.label}
</span>
<span className="text-start text-xs text-muted-foreground">
+${feature.price}/mo
</span>
</div>
</div>
</Button>
<Switch
aria-label={feature.label}
checked={feature.enabled}
onCheckedChange={() => toggleFeature(feature.id)}
/>
</div>
))}
</div>
</div>
<div className="flex flex-col gap-6 rounded-4xl bg-muted p-8">
<div className="flex flex-col gap-2">
<h3 className="text-xl font-medium text-foreground">Your plan</h3>
<p className="text-sm text-muted-foreground">
Base plan + {features.filter((f) => f.enabled).length} add-ons
selected
</p>
</div>
<div className="flex flex-col gap-1">
<div className="flex items-baseline gap-1">
<span className="text-7xl font-semibold tracking-tight text-foreground">
$
</span>
<AnimatePresence mode="popLayout">
{priceChars.map((char, idx) => (
<motion.span
key={`${totalPrice}-${char}-${idx}`}
variants={animVariant}
initial="initial"
animate="animate"
exit="exit"
custom={idx}
className="inline-block text-7xl font-semibold tracking-tight text-foreground"
>
{char}
</motion.span>
))}
</AnimatePresence>
<span key="period" className="text-xl text-muted-foreground">
/month
</span>
</div>
<p className="text-sm text-muted-foreground">
Base: ${basePrice} + Add-ons: ${totalPrice - basePrice}
</p>
</div>
<FancyButton size="lg" className="w-full">
Start free trial
<ArrowUpRightIcon data-icon="inline-end" />
</FancyButton>
<div className="flex flex-col gap-3">
<p className="text-sm font-medium text-foreground">
Always included
</p>
<ul className="flex flex-col gap-3">
{includedFeatures.map((feature) => (
<li key={feature} className="flex items-start gap-3">
<div className="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full bg-primary">
<CheckIcon className="size-3 text-primary-foreground" />
</div>
<span className="text-sm text-muted-foreground">
{feature}
</span>
</li>
))}
</ul>
</div>
</div>
</div>
</div>
</section>
)
}

























