"use client";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { motion } from "motion/react";
import { useEffect, useRef, useState } from "react";
type PriceFlowProps = {
value: number;
className?: string;
};
function PriceFlow({ value, className = "" }: PriceFlowProps) {
const [prevValue, setPrevValue] = useState(value);
const prevTensRef = useRef<HTMLSpanElement>(null);
const nextTensRef = useRef<HTMLSpanElement>(null);
const prevOnesRef = useRef<HTMLSpanElement>(null);
const nextOnesRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
if (value !== prevValue) {
const prevTens = prevTensRef.current;
const nextTens = nextTensRef.current;
const prevOnes = prevOnesRef.current;
const nextOnes = nextOnesRef.current;
if (
prevTens &&
nextTens &&
Math.floor(value / 10) !== Math.floor(prevValue / 10)
) {
if (Math.floor(value / 10) > Math.floor(prevValue / 10)) {
prevTens.classList.add("slide-out-up");
nextTens.classList.add("slide-in-up");
} else {
prevTens.classList.add("slide-out-down");
nextTens.classList.add("slide-in-down");
}
const handleTensAnimationEnd = () => {
prevTens.classList.remove("slide-out-up", "slide-out-down");
nextTens.classList.remove("slide-in-up", "slide-in-down");
prevTens.removeEventListener("animationend", handleTensAnimationEnd);
};
prevTens.addEventListener("animationend", handleTensAnimationEnd);
}
if (prevOnes && nextOnes && value % 10 !== prevValue % 10) {
setTimeout(() => {
if (value % 10 > prevValue % 10) {
prevOnes.classList.add("slide-out-up");
nextOnes.classList.add("slide-in-up");
} else {
prevOnes.classList.add("slide-out-down");
nextOnes.classList.add("slide-in-down");
}
const handleOnesAnimationEnd = () => {
prevOnes.classList.remove("slide-out-up", "slide-out-down");
nextOnes.classList.remove("slide-in-up", "slide-in-down");
prevOnes.removeEventListener(
"animationend",
handleOnesAnimationEnd
);
};
prevOnes.addEventListener("animationend", handleOnesAnimationEnd);
}, 50);
}
setPrevValue(value);
}
}, [value, prevValue]);
const formatValue = (val: number) => val.toString().padStart(2, "0");
const prevFormatted = formatValue(prevValue);
const currentFormatted = formatValue(value);
return (
<span className={cn("relative inline-flex items-center", className)}>
<span className="relative inline-block overflow-hidden">
<span
className="absolute inset-0 flex items-center justify-center"
ref={prevTensRef}
style={{ transform: "translateY(-100%)" }}
>
{prevFormatted[0]}
</span>
<span
className="flex items-center justify-center"
ref={nextTensRef}
style={{ transform: "translateY(0%)" }}
>
{currentFormatted[0]}
</span>
</span>
<span className="relative inline-block overflow-hidden">
<span
className="absolute inset-0 flex items-center justify-center"
ref={prevOnesRef}
style={{ transform: "translateY(-100%)" }}
>
{prevFormatted[1]}
</span>
<span
className="flex items-center justify-center"
ref={nextOnesRef}
style={{ transform: "translateY(0%)" }}
>
{currentFormatted[1]}
</span>
</span>
</span>
);
}
export function PricingSimple() {
const [isAnnual, setIsAnnual] = useState(true);
return (
<section>
<div className="relative bg-muted/50 py-16 md:py-32">
<div className="mx-auto max-w-5xl px-6">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-balance font-bold text-3xl md:text-4xl lg:text-5xl lg:tracking-tight">
Simple pricing for everyone
</h2>
<p className="mx-auto mt-4 max-w-xl text-balance text-foreground/70 text-lg">
One plan, all features. No hidden fees, no complicated tiers.
</p>
<div className="my-12">
<div
className="relative mx-auto grid w-fit grid-cols-2 rounded-full border bg-background p-1 *:block *:h-8 *:w-24 *:rounded-full *:text-foreground *:text-sm *:hover:opacity-75"
data-period={isAnnual ? "annually" : "monthly"}
>
<div
aria-hidden="true"
className={`pointer-events-none absolute inset-1 w-1/2 rounded-full border border-transparent bg-brand shadow ring-1 ring-foreground/5 transition-transform duration-500 ease-in-out ${
isAnnual ? "translate-x-full" : "translate-x-0"
}`}
/>
<button
className="relative duration-500 data-[active=true]:font-medium data-[active=true]:text-white"
data-active={!isAnnual}
onClick={() => setIsAnnual(false)}
type="button"
>
Monthly
</button>
<button
className="relative duration-500 data-[active=true]:font-medium data-[active=true]:text-white"
data-active={isAnnual}
onClick={() => setIsAnnual(true)}
type="button"
>
Annually
</button>
</div>
<div className="mt-3 text-center text-xs">
<span className="font-medium text-brand">Save 20%</span> On
Annual Billing
</div>
</div>
</div>
<div className="container">
<div className="mx-auto max-w-md">
<motion.div
animate={{ opacity: 1, y: 0 }}
className="group relative flex h-[650px] cursor-pointer flex-col overflow-hidden rounded-2xl border bg-background p-8"
data-animate-card
initial={{ opacity: 0, y: 40 }}
transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
>
{/* Gradient Accent */}
<div className="gradient-accent absolute top-0 right-0 h-4 w-32 rounded-bl-2xl bg-gradient-to-r from-green-400 via-blue-400 to-purple-400" />
<div className="card-content relative z-10 flex h-full flex-col">
{/* Title */}
<h3 className="mb-4 font-bold text-2xl text-foreground">
Pro
</h3>
{/* Price & Duration */}
<div className="mb-6">
<span className="font-semibold text-3xl text-foreground">
<PriceFlow value={isAnnual ? 15 : 19} />€
</span>
<span className="mx-2 text-foreground/70">•</span>
<span className="text-foreground/70">
Perfect for individuals
</span>
</div>
{/* CTA Button */}
<button
className="mb-6 inline-flex h-10 w-full cursor-pointer items-center justify-center gap-2 rounded-md bg-foreground px-4 py-2 font-medium text-background text-sm transition-colors hover:bg-foreground/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
type="button"
>
Get Started
</button>
{/* Description */}
<p className="mb-6 flex-grow text-foreground/70 text-sm leading-relaxed">
Everything you need to build and deploy amazing
applications. Simple, powerful, and affordable.
</p>
{/* What's Included */}
<div className="space-y-4">
<h4 className="font-medium text-foreground/70 text-xs uppercase tracking-wider">
What's included:
</h4>
<ul className="space-y-3">
{[
"Unlimited Projects",
"Email Support",
"All Features",
"Advanced Analytics",
"Team Collaboration",
"Custom Domains",
"Priority Updates",
"API Access",
].map((item) => (
<li
className="flex items-center gap-3 text-foreground text-sm"
key={item}
>
<div className="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-foreground">
<svg
aria-hidden="true"
className="h-2 w-2 text-background"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
clipRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
fillRule="evenodd"
/>
</svg>
</div>
{item}
</li>
))}
</ul>
</div>
</div>
</motion.div>
</div>
</div>
</div>
</div>
</section>
);
}
export default PricingSimple;