// Dynamic effects layer — cursor glow, parallax, counters, magnetic hover const { useEffect: useEffectDyn, useState: useStateDyn, useRef: useRefDyn } = React; // ────── Cursor spotlight (desktop only) ────── function CursorGlow() { const ref = useRefDyn(null); useEffectDyn(() => { if (matchMedia('(hover: none)').matches) return; const el = ref.current; let raf = 0; let tx = window.innerWidth / 2, ty = window.innerHeight / 2; let x = tx, y = ty; const onMove = (e) => { tx = e.clientX; ty = e.clientY; }; const tick = () => { x += (tx - x) * 0.16; y += (ty - y) * 0.16; if (el) el.style.transform = `translate3d(${x}px, ${y}px, 0)`; raf = requestAnimationFrame(tick); }; window.addEventListener('mousemove', onMove); raf = requestAnimationFrame(tick); return () => { window.removeEventListener('mousemove', onMove); cancelAnimationFrame(raf); }; }, []); return ; } // ────── Scroll progress bar ────── function ScrollProgress() { const ref = useRefDyn(null); useEffectDyn(() => { const onScroll = () => { const max = document.documentElement.scrollHeight - window.innerHeight; const p = max > 0 ? window.scrollY / max : 0; if (ref.current) ref.current.style.transform = `scaleX(${p})`; }; onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); return
; } // ────── Animated counter (intersection-triggered) ────── function Counter({ to, suffix = '', prefix = '', duration = 1400, accent }) { const [val, setVal] = useStateDyn(0); const ref = useRefDyn(null); useEffectDyn(() => { if (typeof to !== 'number') { setVal(to); return; } const el = ref.current; if (!el) return; let raf = 0, started = false, start = 0; const easeOut = (t) => 1 - Math.pow(1 - t, 3); const animate = (ts) => { if (!start) start = ts; const t = Math.min((ts - start) / duration, 1); setVal(Math.round(easeOut(t) * to)); if (t < 1) raf = requestAnimationFrame(animate); }; const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting && !started) { started = true; raf = requestAnimationFrame(animate); io.unobserve(e.target); } }); }, { threshold: 0.4 }); io.observe(el); return () => { cancelAnimationFrame(raf); io.disconnect(); }; }, [to, duration]); return ( {prefix}{typeof to === 'number' ? val : to}{suffix} ); } // ────── Magnetic / tilt wrapper ────── function useTilt(strength = 8) { const ref = useRefDyn(null); useEffectDyn(() => { const el = ref.current; if (!el || matchMedia('(hover: none)').matches) return; const onMove = (e) => { const r = el.getBoundingClientRect(); const px = (e.clientX - r.left) / r.width - 0.5; const py = (e.clientY - r.top) / r.height - 0.5; el.style.transform = `perspective(900px) rotateX(${-py * strength}deg) rotateY(${px * strength}deg)`; }; const onLeave = () => { el.style.transform = ''; }; el.addEventListener('mousemove', onMove); el.addEventListener('mouseleave', onLeave); return () => { el.removeEventListener('mousemove', onMove); el.removeEventListener('mouseleave', onLeave); }; }, [strength]); return ref; } // ────── Hero parallax ────── function useHeroParallax(selector) { useEffectDyn(() => { if (matchMedia('(hover: none)').matches) return; const el = document.querySelector(selector); if (!el) return; let raf = 0; const onScroll = () => { cancelAnimationFrame(raf); raf = requestAnimationFrame(() => { const y = window.scrollY; el.style.setProperty('--hero-shift', `${Math.min(y * 0.3, 200)}px`); el.style.setProperty('--hero-fade', `${Math.max(1 - y / 500, 0)}`); }); }; window.addEventListener('scroll', onScroll, { passive: true }); return () => { window.removeEventListener('scroll', onScroll); cancelAnimationFrame(raf); }; }, [selector]); } // ────── Type-on text ────── function TypeOn({ text, delay = 0, speed = 28, className }) { const [shown, setShown] = useStateDyn(''); const [done, setDone] = useStateDyn(false); useEffectDyn(() => { let i = 0; let t = setTimeout(function tick() { if (i <= text.length) { setShown(text.slice(0, i)); i++; t = setTimeout(tick, speed); } else { setDone(true); } }, delay); return () => clearTimeout(t); }, [text, delay, speed]); return {shown}{!done && }; } Object.assign(window, { CursorGlow, ScrollProgress, Counter, useTilt, useHeroParallax, TypeOn });