/* Site-wide micro-interaction layer: custom cursor, scroll progress, magnetic buttons,
card tilt, ripple clicks, count-up numbers. All opt out under prefers-reduced-motion. */
const NO_MOTION = () => typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function CursorHalo() {
const dot = React.useRef(null), ring = React.useRef(null);
React.useEffect(() => {
if (NO_MOTION() || window.matchMedia('(pointer: coarse)').matches) return;
let x = innerWidth / 2, y = innerHeight / 2, rx = x, ry = y, raf = 0;
const move = (e) => { x = e.clientX; y = e.clientY; };
const over = (e) => {
const hit = e.target.closest('a,button,[data-cursor="grow"]');
if (ring.current) ring.current.toggleAttribute('data-grow', !!hit);
};
const loop = () => {
raf = requestAnimationFrame(loop);
rx += (x - rx) * 0.16; ry += (y - ry) * 0.16;
if (dot.current) dot.current.style.transform = 'translate3d(' + x + 'px,' + y + 'px,0)';
if (ring.current) ring.current.style.transform = 'translate3d(' + rx + 'px,' + ry + 'px,0)';
};
loop();
window.addEventListener('pointermove', move, { passive: true });
window.addEventListener('pointerover', over, { passive: true });
document.documentElement.setAttribute('data-cursor-on', '');
return () => {
cancelAnimationFrame(raf);
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerover', over);
document.documentElement.removeAttribute('data-cursor-on');
};
}, []);
return (<>
>);
}
function ScrollProgress() {
const bar = React.useRef(null);
React.useEffect(() => {
const on = () => {
const max = document.documentElement.scrollHeight - innerHeight;
if (bar.current) bar.current.style.transform = 'scaleX(' + (max > 0 ? scrollY / max : 0) + ')';
};
on();
window.addEventListener('scroll', on, { passive: true });
window.addEventListener('resize', on);
return () => { window.removeEventListener('scroll', on); window.removeEventListener('resize', on); };
}, []);
return ;
}
/* magnetic pull + ripple on every button, tilt on [data-tilt] — delegated, so it
also covers nodes rendered later */
function useSiteInteractions() {
React.useEffect(() => {
if (NO_MOTION()) return;
const onMove = (e) => {
const mag = e.target.closest('[data-magnetic]');
if (mag) {
const r = mag.getBoundingClientRect();
mag.style.transform = 'translate(' + (e.clientX - r.left - r.width / 2) * 0.14 + 'px,' + (e.clientY - r.top - r.height / 2) * 0.2 + 'px)';
}
const tilt = e.target.closest('[data-tilt]');
if (tilt) {
const r = tilt.getBoundingClientRect();
const nx = (e.clientX - r.left) / r.width - 0.5, ny = (e.clientY - r.top) / r.height - 0.5;
tilt.style.transform = 'perspective(900px) rotateY(' + nx * 7 + 'deg) rotateX(' + -ny * 7 + 'deg) translateZ(0)';
tilt.style.setProperty('--mx', (nx * 100 + 50) + '%');
tilt.style.setProperty('--my', (ny * 100 + 50) + '%');
}
};
const onOut = (e) => {
const n = e.target.closest('[data-magnetic],[data-tilt]');
if (n) n.style.transform = '';
};
const onDown = (e) => {
const b = e.target.closest('button,.zx-btn,[data-ripple]');
if (!b) return;
const r = b.getBoundingClientRect();
const s = document.createElement('span');
s.className = 'zx-ripple';
s.style.left = (e.clientX - r.left) + 'px';
s.style.top = (e.clientY - r.top) + 'px';
if (getComputedStyle(b).position === 'static') b.style.position = 'relative';
b.appendChild(s);
setTimeout(() => s.remove(), 620);
};
document.addEventListener('pointermove', onMove);
document.addEventListener('pointerout', onOut);
document.addEventListener('pointerdown', onDown);
return () => {
document.removeEventListener('pointermove', onMove);
document.removeEventListener('pointerout', onOut);
document.removeEventListener('pointerdown', onDown);
};
}, []);
}
function CountUp({ value, suffix = '', prefix = '', duration = 1400 }) {
const [n, setN] = React.useState(0);
const ref = React.useRef(null);
React.useEffect(() => {
if (NO_MOTION()) { setN(value); return; }
const io = new IntersectionObserver(([e]) => {
if (!e.isIntersecting) return;
io.disconnect();
const t0 = performance.now();
const step = (t) => {
const p = Math.min((t - t0) / duration, 1);
setN(Math.round(value * (1 - Math.pow(1 - p, 3))));
if (p < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
}, { threshold: 0.4 });
if (ref.current) io.observe(ref.current);
return () => io.disconnect();
}, [value, duration]);
return {prefix}{n.toLocaleString('en-IN')}{suffix};
}
Object.assign(window, { CursorHalo, ScrollProgress, useSiteInteractions, CountUp });