Next.js 14 website with standalone output configured for Docker deployment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
46 lines
1.1 KiB
TypeScript
46 lines
1.1 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { useInView } from "framer-motion";
|
|
|
|
interface AnimatedCounterProps {
|
|
target: number;
|
|
suffix?: string;
|
|
prefix?: string;
|
|
duration?: number;
|
|
}
|
|
|
|
export function AnimatedCounter({
|
|
target,
|
|
suffix = "",
|
|
prefix = "",
|
|
duration = 2000,
|
|
}: AnimatedCounterProps) {
|
|
const [count, setCount] = useState(0);
|
|
const ref = useRef(null);
|
|
const isInView = useInView(ref, { once: true });
|
|
const hasAnimated = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (isInView && !hasAnimated.current) {
|
|
hasAnimated.current = true;
|
|
const startTime = Date.now();
|
|
const timer = setInterval(() => {
|
|
const elapsed = Date.now() - startTime;
|
|
const progress = Math.min(elapsed / duration, 1);
|
|
const eased = 1 - Math.pow(1 - progress, 3);
|
|
setCount(Math.floor(eased * target));
|
|
if (progress >= 1) clearInterval(timer);
|
|
}, 16);
|
|
return () => clearInterval(timer);
|
|
}
|
|
}, [isInView, target, duration]);
|
|
|
|
return (
|
|
<span ref={ref}>
|
|
{prefix}
|
|
{count}
|
|
{suffix}
|
|
</span>
|
|
);
|
|
}
|