Classic Counter
0123456789
0123456789
0123456789
Library setup
npx shadcn-ui@latest add button
Code
"use client";
import { MotionValue, motion, useSpring, useTransform } from "framer-motion";
import { Minus, Plus } from "lucide-react";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
const Number = ({ mv, number }: { mv: MotionValue; number: number }) => {
const y = useTransform(mv, (latest) => {
const height = 32;
const offset = (10 + number - (latest % 10)) % 10;
return (offset > 5 ? offset - 10 : offset) * height;
});
const scale = useTransform(y, (y) => {
const absY = Math.abs(y);
return absY > 160 ? 0.5 : absY < 96 ? 1 : 1 - ((absY - 96) / 64) * 0.5;
});
return (
<motion.span
style={{ y, scale }}
className="absolute inset-0 flex items-center justify-center"
>
{number}
</motion.span>
);
};
const Container = ({ value, place }: { value: number; place: number }) => {
const digit = Math.floor(value / place) % 10;
const animatedValue = useSpring(digit);
useEffect(() => animatedValue.set(digit), [animatedValue, digit]);
return (
<div className="relative size-8">
{Array.from({ length: 10 }, (_, index) => (
<Number mv={animatedValue} number={index} key={index} />
))}
</div>
);
};
const ClassicCounter = () => {
const [count, setCount] = useState(0);
return (
<div className="flex h-full w-full items-center justify-center gap-x-4 overflow-hidden">
<Button
variant="secondary"
size="icon"
className="size-7 rounded-full p-1.5"
onClick={() => setCount(count + 1)}
aria-label="Increment counter"
>
<Plus />
</Button>
<div className="relative flex h-24 items-center overflow-hidden">
{[100, 10, 1].map((place) => (
<Container value={count} place={place} key={place} />
))}
<div className="pointer-events-none absolute inset-0 bg-gradient-to-b from-background via-transparent to-background" />
</div>
<Button
variant="secondary"
size="icon"
className="size-7 rounded-full p-1.5"
onClick={() => count > 0 && count < 999 && setCount(count - 1)}
aria-label="Decrement counter"
>
<Minus />
</Button>
</div>
);
};
export default ClassicCounter;