/* ============================================================ CHAINUS — interaction hooks + custom cursor Exported to window: CustomCursor, useReveal, Reveal, MouseGlow, useMagnetic, useTilt, useCountUp ============================================================ */ const { useRef, useEffect, useState, useCallback } = React; function isFinePointer() { return window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches; } function reduceMotion() { return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; } /* ---- Custom cursor: dot + lerped ring, grows over interactive els ---- */ function CustomCursor({ enabled }) { const dotRef = useRef(null); const ringRef = useRef(null); const pos = useRef({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); const ring = useRef({ x: pos.current.x, y: pos.current.y }); const dragHeld = useRef(false); const raf = useRef(0); useEffect(() => { const html = document.documentElement; if (!enabled || !isFinePointer()) { html.classList.remove('cc-on'); return; } html.classList.add('cc-on'); const onMove = (e) => { pos.current.x = e.clientX; pos.current.y = e.clientY; if (dotRef.current) dotRef.current.style.transform = `translate(${e.clientX}px, ${e.clientY}px)`; const t = e.target.closest('a, button, .magnetic, [data-cursor="link"]'); const d = e.target.closest('[data-cursor="drag"]'); const dragging = !!d || dragHeld.current; html.classList.toggle('cc-link', !!t && !dragging); html.classList.toggle('cc-drag', dragging); }; const onDown = () => { dragHeld.current = html.classList.contains('cc-drag'); html.classList.toggle('cc-grabbing', dragHeld.current); if (ringRef.current) ringRef.current.style.opacity = '1'; }; const onUp = () => { dragHeld.current = false; html.classList.remove('cc-grabbing'); if (ringRef.current) ringRef.current.style.opacity = '1'; }; const onLeave = () => { dragHeld.current = false; html.classList.remove('cc-grabbing'); if (dotRef.current) dotRef.current.style.opacity = '0'; if (ringRef.current) ringRef.current.style.opacity = '0'; }; const onEnter = () => { if (dotRef.current) dotRef.current.style.opacity = '1'; if (ringRef.current) ringRef.current.style.opacity = '1'; }; const loop = () => { const speed = dragHeld.current ? 0.42 : 0.18; ring.current.x += (pos.current.x - ring.current.x) * speed; ring.current.y += (pos.current.y - ring.current.y) * speed; if (ringRef.current) ringRef.current.style.transform = `translate(${ring.current.x}px, ${ring.current.y}px)`; raf.current = requestAnimationFrame(loop); }; raf.current = requestAnimationFrame(loop); window.addEventListener('mousemove', onMove, { passive: true }); window.addEventListener('mousedown', onDown); window.addEventListener('mouseup', onUp); document.addEventListener('mouseleave', onLeave); document.addEventListener('mouseenter', onEnter); return () => { cancelAnimationFrame(raf.current); window.removeEventListener('mousemove', onMove); window.removeEventListener('mousedown', onDown); window.removeEventListener('mouseup', onUp); document.removeEventListener('mouseleave', onLeave); document.removeEventListener('mouseenter', onEnter); html.classList.remove('cc-on', 'cc-link', 'cc-drag', 'cc-grabbing'); }; }, [enabled]); return (
); } /* ---- Mouse-following background glow (sets --mx/--my on target) ---- */ function MouseGlow({ enabled }) { useEffect(() => { if (!enabled || !isFinePointer()) return; let raf = 0; const target = { x: 50, y: 30 }; const cur = { x: 50, y: 30 }; const onMove = (e) => { target.x = (e.clientX / window.innerWidth) * 100; target.y = (e.clientY / window.innerHeight) * 100; }; const loop = () => { cur.x += (target.x - cur.x) * 0.06; cur.y += (target.y - cur.y) * 0.06; document.documentElement.style.setProperty('--mx', cur.x + '%'); document.documentElement.style.setProperty('--my', cur.y + '%'); raf = requestAnimationFrame(loop); }; raf = requestAnimationFrame(loop); window.addEventListener('mousemove', onMove, { passive: true }); return () => { cancelAnimationFrame(raf); window.removeEventListener('mousemove', onMove); }; }, [enabled]); return null; } /* ---- Scroll reveal via IntersectionObserver ---- */ function useReveal() { useEffect(() => { if (reduceMotion()) { document.querySelectorAll('.reveal, .reveal-scale').forEach((el) => el.classList.add('in')); return; } const io = new IntersectionObserver((entries) => { entries.forEach((en) => { if (en.isIntersecting) { en.target.classList.add('in'); io.unobserve(en.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' }); const els = document.querySelectorAll('.reveal, .reveal-scale'); els.forEach((el) => io.observe(el)); return () => io.disconnect(); }); } /* convenience wrapper */ function Reveal({ children, className = '', delay = 0, scale = false, as = 'div', ...rest }) { const Tag = as; return ( {children} ); } /* ---- Magnetic element: pulls toward cursor ---- */ function useMagnetic(strength = 0.4) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el || !isFinePointer() || reduceMotion()) return; let raf = 0; const cur = { x: 0, y: 0 }; const tgt = { x: 0, y: 0 }; const onMove = (e) => { const r = el.getBoundingClientRect(); const cx = r.left + r.width / 2, cy = r.top + r.height / 2; tgt.x = (e.clientX - cx) * strength; tgt.y = (e.clientY - cy) * strength; }; const onLeave = () => { tgt.x = 0; tgt.y = 0; }; const loop = () => { cur.x += (tgt.x - cur.x) * 0.15; cur.y += (tgt.y - cur.y) * 0.15; el.style.transform = `translate(${cur.x}px, ${cur.y}px)`; raf = requestAnimationFrame(loop); }; raf = requestAnimationFrame(loop); el.addEventListener('mousemove', onMove); el.addEventListener('mouseleave', onLeave); return () => { cancelAnimationFrame(raf); el.removeEventListener('mousemove', onMove); el.removeEventListener('mouseleave', onLeave); }; }, [strength]); return ref; } /* ---- 3D tilt that also responds to global mouse (parallax) ---- */ function useTilt(max = 10, parallax = false) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el || !isFinePointer() || reduceMotion()) return; let raf = 0; const tgt = { rx: 0, ry: 0 }; const cur = { rx: 0, ry: 0 }; const onMove = (e) => { let nx, ny; if (parallax) { nx = (e.clientX / window.innerWidth) * 2 - 1; ny = (e.clientY / window.innerHeight) * 2 - 1; } else { const r = el.getBoundingClientRect(); nx = ((e.clientX - r.left) / r.width) * 2 - 1; ny = ((e.clientY - r.top) / r.height) * 2 - 1; } tgt.ry = nx * max; tgt.rx = -ny * max; }; const onLeave = () => { tgt.rx = 0; tgt.ry = 0; }; const loop = () => { cur.rx += (tgt.rx - cur.rx) * 0.08; cur.ry += (tgt.ry - cur.ry) * 0.08; el.style.transform = `perspective(1100px) rotateX(${cur.rx}deg) rotateY(${cur.ry}deg)`; raf = requestAnimationFrame(loop); }; raf = requestAnimationFrame(loop); const listenEl = parallax ? window : el; listenEl.addEventListener('mousemove', onMove, { passive: true }); if (!parallax) el.addEventListener('mouseleave', onLeave); return () => { cancelAnimationFrame(raf); listenEl.removeEventListener('mousemove', onMove); if (!parallax) el.removeEventListener('mouseleave', onLeave); }; }, [max, parallax]); return ref; } /* ---- count-up on first reveal ---- */ function useCountUp(end, dur = 1400, suffix = '') { const ref = useRef(null); const [txt, setTxt] = useState('0' + suffix); useEffect(() => { const el = ref.current; if (!el) return; if (reduceMotion()) { setTxt(end.toLocaleString() + suffix); return; } let started = false; const io = new IntersectionObserver((es) => { es.forEach((e) => { if (e.isIntersecting && !started) { started = true; const t0 = performance.now(); const tick = (t) => { const p = Math.min(1, (t - t0) / dur); const eased = 1 - Math.pow(1 - p, 3); setTxt(Math.round(end * eased).toLocaleString() + suffix); if (p < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); io.disconnect(); } }); }, { threshold: 0.5 }); io.observe(el); return () => io.disconnect(); }, [end, dur, suffix]); return [ref, txt]; } Object.assign(window, { CustomCursor, MouseGlow, useReveal, Reveal, useMagnetic, useTilt, useCountUp, isFinePointer, reduceMotion, // expose React hooks so every Babel file can use bare identifiers useRef, useEffect, useState, useCallback, });