From 265a4f576fc2733104317eabe8144a735a566fb6 Mon Sep 17 00:00:00 2001 From: flexykrn Date: Wed, 17 Jun 2026 17:54:30 +0530 Subject: [PATCH] Docusaurus migration - website-src-hooks Part 69 of the Docusaurus migration split. --- website/src/hooks/useCountUp.ts | 156 ++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 website/src/hooks/useCountUp.ts diff --git a/website/src/hooks/useCountUp.ts b/website/src/hooks/useCountUp.ts new file mode 100644 index 00000000..f9722ee0 --- /dev/null +++ b/website/src/hooks/useCountUp.ts @@ -0,0 +1,156 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; + +function parseValue(value: string): { num: number; suffix: string; prefix: string } { + const cleaned = value.replace(/,/g, '').trim(); + const match = cleaned.match(/^([^\d.-]*)([\d.]+)([^\d.]*)$/); + if (match) { + return { prefix: match[1], num: parseFloat(match[2]), suffix: match[3] }; + } + const numMatch = cleaned.match(/[\d.]+/); + return { prefix: '', num: numMatch ? parseFloat(numMatch[0]) : 0, suffix: '' }; +} + +export function useCountUp(targetValue: string, duration = 1500) { + const [displayValue, setDisplayValue] = useState(targetValue); + const [hasStarted, setHasStarted] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el || typeof IntersectionObserver === 'undefined') { + setHasStarted(true); + return; + } + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting && !hasStarted) { + setHasStarted(true); + } + }, + { threshold: 0.3 } + ); + + observer.observe(el); + return () => observer.disconnect(); + }, [hasStarted, targetValue]); + + useEffect(() => { + if (!hasStarted) return; + + const { prefix, num, suffix } = parseValue(targetValue); + const startTime = performance.now(); + let raf = 0; + + const tick = (now: number) => { + const elapsed = now - startTime; + const progress = Math.min(elapsed / duration, 1); + const eased = 1 - Math.pow(1 - progress, 3); + const current = num * eased; + + if (num >= 1000) { + setDisplayValue(`${prefix}${Math.floor(current).toLocaleString()}${suffix}`); + } else if (Number.isInteger(num)) { + setDisplayValue(`${prefix}${Math.floor(current)}${suffix}`); + } else { + setDisplayValue(`${prefix}${current.toFixed(num < 1 ? 4 : 2)}${suffix}`); + } + + if (progress < 1) { + raf = requestAnimationFrame(tick); + } else { + setDisplayValue(targetValue); + } + }; + + raf = requestAnimationFrame(tick); + return () => cancelAnimationFrame(raf); + }, [hasStarted, targetValue, duration]); + + return { ref, displayValue }; +} + +export function useInView(threshold = 0.3) { + const [inView, setInView] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el || typeof IntersectionObserver === 'undefined') { + setInView(true); + return; + } + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) setInView(true); + }, + { threshold } + ); + observer.observe(el); + return () => observer.disconnect(); + }, [threshold]); + + return { ref, inView }; +} + +export function useTypingEffect(words: string[], typingSpeed = 100, deletingSpeed = 50, pause = 2000) { + const [text, setText] = useState(''); + const [wordIndex, setWordIndex] = useState(0); + const [isDeleting, setIsDeleting] = useState(false); + const [showCursor, setShowCursor] = useState(true); + + useEffect(() => { + const currentWord = words[wordIndex]; + let timer: ReturnType; + + if (isDeleting) { + if (text === '') { + setIsDeleting(false); + setWordIndex((prev) => (prev + 1) % words.length); + } else { + timer = setTimeout(() => setText(text.slice(0, -1)), deletingSpeed); + } + } else { + if (text === currentWord) { + timer = setTimeout(() => setIsDeleting(true), pause); + } else { + timer = setTimeout(() => setText(currentWord.slice(0, text.length + 1)), typingSpeed); + } + } + + return () => clearTimeout(timer); + }, [text, isDeleting, wordIndex, words, typingSpeed, deletingSpeed, pause]); + + useEffect(() => { + const blink = setInterval(() => setShowCursor((s) => !s), 530); + return () => clearInterval(blink); + }, []); + + return { text, showCursor }; +} + +export function useScrollProgress() { + const [progress, setProgress] = useState(0); + + useEffect(() => { + let raf = 0; + const onScroll = () => { + if (raf) return; + raf = requestAnimationFrame(() => { + const top = window.scrollY; + const height = document.documentElement.scrollHeight - window.innerHeight; + setProgress(height > 0 ? top / height : 0); + raf = 0; + }); + }; + + window.addEventListener('scroll', onScroll, { passive: true }); + onScroll(); + return () => { + window.removeEventListener('scroll', onScroll); + if (raf) cancelAnimationFrame(raf); + }; + }, []); + + return progress; +}