An interactive pricing card with a draggable slider, animated character transitions, and shuffling price values.
pnpm add motion lucide-react motionCreate components/pricing-card.tsx and paste the code below:
"use client";
import React, { useState, useEffect } from "react";
import { motion, AnimatePresence, useSpring, useTransform } from "motion/react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { User, Users, Briefcase, Building, Minus, Plus } from "lucide-react";
const pricingTiers = [
{ maxUsers: 1000, price: 0, icon: User, color: "text-blue-500", bg: "bg-blue-500/15" },
{ maxUsers: 3000, price: 20, icon: Users, color: "text-purple-500", bg: "bg-purple-500/15" },
{ maxUsers: 6000, price: 50, icon: Briefcase, color: "text-orange-500", bg: "bg-orange-500/15" },
{ maxUsers: 9000, price: 90, icon: Building, color: "text-green-500", bg: "bg-green-500/15" },
];
const MAX_VAL = 10000;
export default function PricingCard() {
const [value, setValue] = useState(1000);
const isEnterprise = value > 9000;
const springValue = useSpring(1000, {
stiffness: 1000,
damping: 100,
mass: 0.1
});
const displayValue = useTransform(springValue, (latest) => {
const val = Math.round(latest);
if (val >= 1000) {
return `${parseFloat((val / 1000).toFixed(1))}k`;
}
return val.toLocaleString();
});
const currentTier = pricingTiers.find(tier => value <= tier.maxUsers) || pricingTiers[pricingTiers.length - 1];
useEffect(() => {
if (!isEnterprise) {
springValue.set(value);
}
}, [value, isEnterprise, springValue]);
const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(parseInt(e.target.value));
};
const increment = () => setValue(prev => Math.min(prev + 1000, MAX_VAL));
const decrement = () => setValue(prev => Math.max(prev - 1000, 1000));
const calculatePrice = (users: number) => {
if (users <= 1000) return 0;
if (users <= 3000) return 20;
if (users <= 6000) return 50;
if (users <= 9000) return 90;
return 0;
};
const price = calculatePrice(value);
const Icon = isEnterprise ? Building : currentTier.icon;
return (
<Card className="w-full max-w-[340px] mx-auto overflow-hidden bg-card border-border text-card-foreground rounded-[2.5rem] transition-colors duration-500 shadow-sm">
<CardContent className="p-6 flex flex-col items-center gap-6">
<div className="text-muted-foreground text-[13px] font-bold tracking-tight uppercase opacity-60">
Scale your business
</div>
<div className="relative h-24 w-full flex items-center justify-center">
<AnimatePresence mode="wait">
<motion.div
key={isEnterprise ? 'ent' : currentTier.maxUsers}
className="relative"
initial={{ scale: 0.8, opacity: 0, y: 10 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.8, opacity: 0, y: -10 }}
transition={{ type: "spring", stiffness: 400, damping: 25 }}
>
<div
className={cn(
"p-5 rounded-[2.2rem] transition-colors duration-500 relative z-10 border border-border/50 shadow-sm",
isEnterprise ? "bg-muted" : currentTier.bg
)}
>
<Icon className={cn("w-10 h-10 transition-colors duration-500", isEnterprise ? "text-muted-foreground" : currentTier.color)} />
</div>
</motion.div>
</AnimatePresence>
</div>
<div className="text-center w-full">
<div className="flex items-baseline justify-center gap-4 h-12">
<span className="text-muted-foreground text-sm font-bold uppercase tracking-wider opacity-60">
{isEnterprise ? "Over" : "Up to"}
</span>
<div className="flex justify-center items-baseline">
<AnimatePresence mode="popLayout" initial={false}>
{isEnterprise ? (
<motion.span
key="ent"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="text-5xl font-black tracking-tighter text-foreground leading-none px-1"
>
10k+
</motion.span>
) : (
<motion.span
key="val"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="text-5xl font-black tracking-tighter text-foreground leading-none px-1"
>
<motion.span>{displayValue}</motion.span>
</motion.span>
)}
</AnimatePresence>
</div>
<span className="text-muted-foreground text-sm font-bold uppercase tracking-wider opacity-60">
Users
</span>
</div>
</div>
<div className="w-full space-y-4">
<div className="relative h-10 flex items-center px-1">
<div className="absolute inset-0 flex justify-between items-center pointer-events-none px-2">
{Array.from({ length: 31 }).map((_, i) => {
const normalizedPos = i / 30;
const sliderPos = (value - 1000) / (MAX_VAL - 1000);
const dist = Math.abs(normalizedPos - sliderPos);
const bump = Math.max(0, 1 - dist * 4);
return (
<motion.div
key={i}
animate={{
height: 4 + (bump * 16),
backgroundColor: dist < 0.04 ? "var(--foreground)" : "var(--muted-foreground)"
}}
style={{
opacity: dist < 0.04 ? 1 : 0.2
}}
transition={{ type: "spring", stiffness: 400, damping: 30, mass: 0.5 }}
className="w-[2px] rounded-full"
/>
);
})}
</div>
<input
type="range"
min="1000"
max={MAX_VAL}
step="100"
value={value}
onChange={handleSliderChange}
className="absolute inset-0 w-full h-full opacity-0 cursor-grab active:cursor-grabbing z-10"
/>
</div>
<div className="flex justify-center items-center gap-5 pt-1">
<span className="text-muted-foreground text-[14px] font-bold min-w-[100px] text-center">
{isEnterprise ? "Enterprise" : (price === 0 ? "Free plan" : `$${price} / month`)}
</span>
<div className="flex gap-2">
<button
onClick={decrement}
className="w-8 h-8 rounded-full bg-muted border border-border flex items-center justify-center hover:bg-accent transition-colors active:scale-90"
>
<Minus className="w-3.5 h-3.5 text-muted-foreground" />
</button>
<button
onClick={increment}
className="w-8 h-8 rounded-full bg-muted border border-border flex items-center justify-center hover:bg-accent transition-colors active:scale-90"
>
<Plus className="w-3.5 h-3.5 text-muted-foreground" />
</button>
</div>
</div>
</div>
<Button
className={cn(
"w-full h-12 rounded-2xl text-[15px] font-bold transition-all duration-300 active:scale-[0.98] mt-2",
isEnterprise
? "bg-foreground text-background hover:opacity-90"
: "bg-[#9B87FF] text-white hover:bg-[#8A75FF]"
)}
>
{isEnterprise ? "Get in touch" : (price === 0 ? "Get started" : `Subscribe for $${price}`)}
</Button>
</CardContent>
</Card>
);
}
import PricingCard from "@/components/pricing-card";
export default function MyComponent() {
return (
<div className="w-full flex items-center justify-center p-4">
<PricingCard />
</div>
);
}