/* ============================================================
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,
});