'use client'; import { forwardRef, HTMLAttributes, ReactNode, useRef, useEffect, useState } from 'react'; import styles from './parallax-container.module.css'; export interface ParallaxContainerProps extends HTMLAttributes { children: ReactNode; speed?: number; offset?: number; disabled?: boolean; mobileBreakpoint?: number; smoothScrolling?: boolean; enable3D?: boolean; gpuAcceleration?: boolean; throttleMs?: number; className?: string; } export interface ParallaxLayerProps extends HTMLAttributes { children: ReactNode; speed?: number; offset?: number; depth?: number; gpu?: boolean; className?: string; } interface LayerData { element: HTMLElement; speed: number; offset: number; depth: number; } export const ParallaxContainer = forwardRef( ( { children, speed = 0.5, offset = 0, disabled = false, mobileBreakpoint = 768, smoothScrolling = false, enable3D = false, gpuAcceleration = true, throttleMs = 16, className = '', style, ...props }, ref ) => { const containerRef = useRef(null); const layersRef = useRef>(new Map()); const [isMobile, setIsMobile] = useState(false); const lastThrottle = useRef(0); useEffect(() => { const container = containerRef.current; if (!container) return; const checkMobile = () => { setIsMobile(window.innerWidth < mobileBreakpoint); }; checkMobile(); window.addEventListener('resize', checkMobile); return () => window.removeEventListener('resize', checkMobile); }, [mobileBreakpoint]); useEffect(() => { if (disabled || isMobile) return; const container = containerRef.current; if (!container) return; let rafId: number; const updateLayers = () => { const scrollTop = window.scrollY; const containerRect = container.getBoundingClientRect(); const containerTop = containerRect.top + scrollTop; layersRef.current.forEach((layer) => { const relativeScroll = scrollTop - containerTop; const parallaxOffset = relativeScroll * layer.speed * speed + layer.offset; if (enable3D && layer.depth !== 0) { layer.element.style.transform = `translate3d(0, ${parallaxOffset}px, ${layer.depth}px)`; } else { layer.element.style.transform = `translate3d(0, ${parallaxOffset}px, 0)`; } }); }; const handleScroll = () => { const now = performance.now(); if (now - lastThrottle.current < throttleMs) { if (rafId) cancelAnimationFrame(rafId); rafId = requestAnimationFrame(() => { updateLayers(); lastThrottle.current = now; }); return; } updateLayers(); lastThrottle.current = now; }; const scanLayers = () => { layersRef.current.clear(); const layerElements = container.querySelectorAll(`.${styles.layer}`); layerElements.forEach((el) => { if (el instanceof HTMLElement) { const layerSpeed = parseFloat(el.dataset.speed || '1'); const layerOffset = parseFloat(el.dataset.offset || '0'); const layerDepth = parseFloat(el.dataset.depth || '0'); layersRef.current.set(el, { element: el, speed: layerSpeed, offset: layerOffset, depth: layerDepth, }); } }); }; const observer = new MutationObserver(scanLayers); observer.observe(container, { childList: true, subtree: true }); window.addEventListener('scroll', handleScroll, { passive: true }); scanLayers(); updateLayers(); return () => { window.removeEventListener('scroll', handleScroll); if (rafId) cancelAnimationFrame(rafId); observer.disconnect(); }; }, [disabled, isMobile, speed, enable3D, throttleMs]); const containerStyle: React.CSSProperties = { '--parallax-speed': speed, '--parallax-offset': `${offset}px`, ...style, } as React.CSSProperties; const containerClasses = [ styles.container, smoothScrolling && styles.smooth, enable3D && styles.perspective, gpuAcceleration && styles.gpu, className, ] .filter(Boolean) .join(' '); return (
{ if (typeof ref === 'function') ref(node); else if (ref) ref.current = node; containerRef.current = node; }} className={containerClasses} style={containerStyle} {...props} > {children}
); } ); ParallaxContainer.displayName = 'ParallaxContainer'; export const ParallaxLayer = forwardRef( ( { children, speed = 1, offset = 0, depth = 0, gpu = true, className = '', style, ...props }, ref ) => { const layerClasses = [ styles.layer, gpu && styles.gpu, className, ] .filter(Boolean) .join(' '); return (
{children}
); } ); ParallaxLayer.displayName = 'ParallaxLayer'; export default ParallaxContainer;