Files
tenext-next/components/shared/Counter.tsx
2026-06-18 11:13:39 +05:30

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>
);
}