'use client'; import { useEffect, useState, RefObject } from 'react'; /** * Calculate scroll progress (0-1) based on element's position in viewport * @param ref - Reference to the element to track * @param offset - Offset from viewport edges (0-1, default 0.1) * @returns progress value between 0 and 1 */ export function useScrollProgress(ref: RefObject, offset: number = 0.1): number { const [progress, setProgress] = useState(0); useEffect(() => { if (!ref.current) return; let rafId: number; const calculateProgress = () => { if (!ref.current) return; const rect = ref.current.getBoundingClientRect(); const viewportHeight = window.innerHeight; const scrollY = window.scrollY; const documentHeight = document.documentElement.scrollHeight; // Element enters viewport from bottom const enterPoint = viewportHeight * (1 - offset); // Element reaches top of viewport const exitPoint = viewportHeight * offset; // Calculate progress: 0 when entering, 1 at 30% viewport (accelerated) const elementCenter = rect.top + rect.height / 2; const totalDistance = enterPoint - exitPoint; const currentDistance = enterPoint - elementCenter; // Accelerate progress to reach 1.0 at 30% viewport height // Scale factor: 1.67 makes progress reach 1.0 at ~30% instead of 50% const rawProgress = (currentDistance / totalDistance) * 1.67; let clampedProgress = Math.max(0, Math.min(1, rawProgress)); // At bottom of page - ensure elements near bottom can reach 100% // Only apply if we're at the very bottom AND this element is below the fold if (scrollY + viewportHeight >= documentHeight - 50 && rect.top < viewportHeight) { clampedProgress = Math.max(clampedProgress, 1); } setProgress(clampedProgress); }; const handleScroll = () => { if (rafId) { cancelAnimationFrame(rafId); } rafId = requestAnimationFrame(calculateProgress); }; // Initial calculation calculateProgress(); window.addEventListener('scroll', handleScroll, { passive: true }); window.addEventListener('resize', handleScroll, { passive: true }); return () => { window.removeEventListener('scroll', handleScroll); window.removeEventListener('resize', handleScroll); if (rafId) { cancelAnimationFrame(rafId); } }; }, [ref, offset]); return progress; } /** * Calculate parallax offset based on scroll position * @param ref - Reference to the element to track * @param speed - Parallax speed multiplier (default 0.5) * @returns offset in pixels */ export function useParallax(ref: RefObject, speed: number = 0.5): number { const [offset, setOffset] = useState(0); useEffect(() => { if (!ref.current) return; let rafId: number; const calculateOffset = () => { if (!ref.current) return; const rect = ref.current.getBoundingClientRect(); const viewportHeight = window.innerHeight; // Calculate offset based on element position relative to viewport const scrolled = viewportHeight - rect.top; const parallaxOffset = scrolled * speed; setOffset(parallaxOffset); }; const handleScroll = () => { if (rafId) { cancelAnimationFrame(rafId); } rafId = requestAnimationFrame(calculateOffset); }; calculateOffset(); window.addEventListener('scroll', handleScroll, { passive: true }); window.addEventListener('resize', handleScroll, { passive: true }); return () => { window.removeEventListener('scroll', handleScroll); window.removeEventListener('resize', handleScroll); if (rafId) { cancelAnimationFrame(rafId); } }; }, [ref, speed]); return offset; }