Files
dcea6ced-2a76-4098-a68c-74a…/src/components/text/TextNumberCount.tsx
Nikolay Pecheniev 5768c6396d Initial commit
2026-02-24 13:17:54 +02:00

106 lines
2.5 KiB
TypeScript

"use client";
import { memo, useState, useEffect, useRef, useCallback } from "react";
import { AnimateNumber } from "motion-number";
interface TextNumberCountProps {
value: number;
format?: Omit<Intl.NumberFormatOptions, "notation"> & {
notation?: Exclude<
Intl.NumberFormatOptions["notation"],
"scientific" | "engineering"
>;
};
locales?: Intl.LocalesArgument;
className?: string;
suffix?: string;
prefix?: string;
animateOnScroll?: boolean;
startFrom?: number;
duration?: number;
threshold?: number;
}
const TextNumberCount = ({
value,
format,
locales = "en-US",
className = "",
suffix,
prefix,
animateOnScroll = false,
startFrom,
duration = 2,
threshold = 0.5,
}: TextNumberCountProps) => {
const initialValue = animateOnScroll ? (startFrom ?? 0) : value;
const [displayValue, setDisplayValue] = useState(initialValue);
const [hasAnimated, setHasAnimated] = useState(false);
const containerRef = useRef<HTMLSpanElement>(null);
const observerRef = useRef<IntersectionObserver | null>(null);
const handleIntersection = useCallback(
(entries: IntersectionObserverEntry[]) => {
const [entry] = entries;
if (entry?.isIntersecting && !hasAnimated) {
setDisplayValue(value);
setHasAnimated(true);
observerRef.current?.disconnect();
}
},
[value, hasAnimated]
);
useEffect(() => {
if (!animateOnScroll || hasAnimated || typeof window === "undefined") {
return;
}
const element = containerRef.current;
if (!element) return;
observerRef.current = new IntersectionObserver(handleIntersection, {
threshold: Math.min(Math.max(threshold, 0), 1),
rootMargin: "0px",
});
observerRef.current.observe(element);
return () => {
observerRef.current?.disconnect();
};
}, [animateOnScroll, hasAnimated, threshold, handleIntersection]);
useEffect(() => {
if (!animateOnScroll) {
setDisplayValue(value);
}
}, [value, animateOnScroll]);
const animateProps = animateOnScroll
? { animate: { duration } }
: {};
const content = (
<AnimateNumber
format={format}
locales={locales}
className={className}
suffix={suffix}
prefix={prefix}
{...animateProps}
>
{displayValue}
</AnimateNumber>
);
if (animateOnScroll) {
return <span ref={containerRef}>{content}</span>;
}
return content;
};
TextNumberCount.displayName = "TextNumberCount";
export default memo(TextNumberCount);