80 lines
2.0 KiB
TypeScript
80 lines
2.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
interface CounterProps {
|
|
target: number;
|
|
suffix?: string;
|
|
prefix?: string;
|
|
duration?: number;
|
|
className?: string;
|
|
title?: string;
|
|
numberClassName?: string;
|
|
titleClassName?: string;
|
|
layout?: "default" | "row-reverse";
|
|
gap?: string;
|
|
style?: React.CSSProperties;
|
|
}
|
|
|
|
export function Counter({
|
|
target,
|
|
suffix = "",
|
|
prefix = "",
|
|
duration = 2000,
|
|
className = "",
|
|
title,
|
|
numberClassName = "",
|
|
titleClassName = "",
|
|
layout = "default",
|
|
gap = "0px",
|
|
style,
|
|
}: CounterProps) {
|
|
const [count, setCount] = useState(0);
|
|
const [hasAnimated, setHasAnimated] = useState(false);
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const el = ref.current;
|
|
if (!el) return;
|
|
|
|
const observer = new IntersectionObserver(
|
|
([entry]) => {
|
|
if (entry.isIntersecting && !hasAnimated) {
|
|
setHasAnimated(true);
|
|
const startTime = performance.now();
|
|
const animate = (now: number) => {
|
|
const elapsed = now - startTime;
|
|
const progress = Math.min(elapsed / duration, 1);
|
|
// ease-out
|
|
const eased = 1 - Math.pow(1 - progress, 3);
|
|
setCount(Math.floor(eased * target));
|
|
if (progress < 1) requestAnimationFrame(animate);
|
|
};
|
|
requestAnimationFrame(animate);
|
|
}
|
|
},
|
|
{ threshold: 0.3 }
|
|
);
|
|
|
|
observer.observe(el);
|
|
return () => observer.disconnect();
|
|
}, [target, duration, hasAnimated]);
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={`${layout === "row-reverse" ? "flex flex-row-reverse items-end" : "flex flex-col"} ${className}`}
|
|
style={{ gap }}
|
|
>
|
|
<div className={`${numberClassName}`} style={style}>
|
|
{prefix && <span>{prefix}</span>}
|
|
<span>{count}</span>
|
|
{suffix && <span>{suffix}</span>}
|
|
</div>
|
|
{title && (
|
|
<div className={`${titleClassName}`}>{title}</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|