59 lines
1.3 KiB
TypeScript
59 lines
1.3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { useInView, useSpring, useTransform, motion } from "framer-motion";
|
|
|
|
interface AnimatedCounterProps {
|
|
value: number;
|
|
suffix?: string;
|
|
prefix?: string;
|
|
duration?: number;
|
|
className?: string;
|
|
}
|
|
|
|
export default function AnimatedCounter({
|
|
value,
|
|
suffix = "",
|
|
prefix = "",
|
|
duration = 2,
|
|
className = "",
|
|
}: AnimatedCounterProps) {
|
|
const ref = useRef<HTMLSpanElement>(null);
|
|
const isInView = useInView(ref, { once: true, margin: "-50px" });
|
|
const [hasAnimated, setHasAnimated] = useState(false);
|
|
|
|
const spring = useSpring(0, {
|
|
stiffness: 50,
|
|
damping: 30,
|
|
duration: duration * 1000,
|
|
});
|
|
|
|
const display = useTransform(spring, (current) => {
|
|
return Math.round(current);
|
|
});
|
|
|
|
const [displayValue, setDisplayValue] = useState(0);
|
|
|
|
useEffect(() => {
|
|
if (isInView && !hasAnimated) {
|
|
setHasAnimated(true);
|
|
spring.set(value);
|
|
}
|
|
}, [isInView, hasAnimated, spring, value]);
|
|
|
|
useEffect(() => {
|
|
const unsubscribe = display.on("change", (v) => {
|
|
setDisplayValue(v);
|
|
});
|
|
return unsubscribe;
|
|
}, [display]);
|
|
|
|
return (
|
|
<motion.span ref={ref} className={className}>
|
|
{prefix}
|
|
{displayValue}
|
|
{suffix}
|
|
</motion.span>
|
|
);
|
|
}
|