Files
pars-elevator/src/components/Counter.tsx
T
Alireza HassaniandCursor 5909899b1e Initial Next.js clone of Pars Elevator (plift.ir).
RTL Meshkee storefront with home/about/contact/products/blog, API auth/contact/blog wiring, and SEO sitemap-config.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 14:55:42 +03:30

65 lines
1.5 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
type Props = {
value: number;
className?: string;
};
export function Counter({ value, className = "" }: Props) {
const [n, setN] = useState(0);
const ref = useRef<HTMLSpanElement>(null);
const started = useRef(false);
useEffect(() => {
const el = ref.current;
if (!el || started.current) return;
const run = () => {
if (started.current) return;
started.current = true;
const duration = 1400;
const start = performance.now();
const tick = (now: number) => {
const t = Math.min(1, (now - start) / duration);
const eased = 1 - Math.pow(1 - t, 3);
setN(Math.round(value * eased));
if (t < 1) requestAnimationFrame(tick);
else setN(value);
};
requestAnimationFrame(tick);
};
const isVisible = () => {
const rect = el.getBoundingClientRect();
const vh = window.innerHeight || document.documentElement.clientHeight;
return rect.top < vh * 0.92 && rect.bottom > vh * 0.08;
};
if (isVisible()) {
run();
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
run();
observer.disconnect();
}
},
{ threshold: 0.2 },
);
observer.observe(el);
return () => observer.disconnect();
}, [value]);
return (
<span ref={ref} className={className} dir="ltr">
{n.toLocaleString("en-US")}
</span>
);
}