/* store-anim.jsx — motion & interaction primitives for the store.
Reveal / CountUp (scroll), CursorGlow, useTilt, useParallax, useCountdown.
One shared pointer source (FxPointer) drives glow + parallax.
Everything honors prefers-reduced-motion. Exported to window. */
const _fxReduce = () =>
typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ---- shared pointer ---- */
const FxPointer = (function () {
let s = { x: 0, y: 0, nx: 0, ny: 0 };
const subs = new Set();
if (typeof window !== "undefined") {
window.addEventListener("mousemove", (e) => {
s = {
x: e.clientX,
y: e.clientY,
nx: (e.clientX / window.innerWidth) * 2 - 1,
ny: (e.clientY / window.innerHeight) * 2 - 1,
};
subs.forEach((fn) => fn(s));
}, { passive: true });
}
return { subscribe(fn) { subs.add(fn); return () => subs.delete(fn); }, get() { return s; } };
})();
/* ---- Reveal (scroll fade + rise) ---- */
function Reveal({ children, y = 26, delay = 0, dur = 720, className = "", style = {}, as: Tag = "div" }) {
const ref = React.useRef(null);
const [shown, setShown] = React.useState(false);
React.useEffect(() => {
const el = ref.current;
if (!el) return;
if (_fxReduce()) { setShown(true); return; }
const io = new IntersectionObserver((es) => {
es.forEach((e) => { if (e.isIntersecting) { setShown(true); io.unobserve(el); } });
}, { threshold: 0.14, rootMargin: "0px 0px -6% 0px" });
io.observe(el);
return () => io.disconnect();
}, []);
const ease = "cubic-bezier(.21,.68,.24,1)";
return (
{children}
);
}
/* ---- CountUp ---- */
function CountUp({ to, from = 0, dur = 1500, prefix = "", suffix = "", decimals = 0, className = "", style = {} }) {
const ref = React.useRef(null);
const started = React.useRef(false);
const [val, setVal] = React.useState(from);
React.useEffect(() => {
const el = ref.current;
if (!el) return;
if (_fxReduce()) { setVal(to); return; }
const io = new IntersectionObserver((es) => {
es.forEach((e) => {
if (e.isIntersecting && !started.current) {
started.current = true;
const t0 = performance.now();
const tick = (now) => {
const p = Math.min(1, (now - t0) / dur);
const eased = 1 - Math.pow(1 - p, 3);
setVal(from + (to - from) * eased);
if (p < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
io.unobserve(el);
}
});
}, { threshold: 0.5 });
io.observe(el);
return () => io.disconnect();
}, []);
return {prefix}{val.toFixed(decimals)}{suffix};
}
/* ---- CursorGlow — canvas cursor system: breathing glow + trail + sparkles.
One rAF loop, transform/opacity only, capped particle pool. ---- */
function CursorGlow() {
const cvsRef = React.useRef(null);
React.useEffect(() => {
if (_fxReduce()) return;
const cvs = cvsRef.current;
const ctx = cvs.getContext("2d");
const dpr = Math.min(window.devicePixelRatio || 1, 2);
let w = 0, h = 0;
const resize = () => {
w = window.innerWidth; h = window.innerHeight;
cvs.width = w * dpr; cvs.height = h * dpr;
cvs.style.width = w + "px"; cvs.style.height = h + "px";
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
};
resize();
window.addEventListener("resize", resize);
let mx = -600, my = -600, ex = -600, ey = -600, lx = -600, ly = -600, present = 0;
const trail = [], sparks = [];
const rand = (a, b) => a + Math.random() * (b - a);
const unsub = FxPointer.subscribe(({ x, y }) => { mx = x; my = y; });
let raf;
const loop = () => {
ex += (mx - ex) * 0.18; ey += (my - ey) * 0.18;
const speed = Math.hypot(mx - lx, my - ly);
lx = mx; ly = my;
present += ((mx < 0 ? 0 : 1) - present) * 0.07;
trail.push({ x: ex, y: ey }); if (trail.length > 20) trail.shift();
if (speed > 5 && mx > 0) {
const n = Math.min(3, 1 + Math.floor(speed / 12));
for (let i = 0; i < n; i++) sparks.push({ x: mx + rand(-7, 7), y: my + rand(-7, 7), vx: rand(-0.7, 0.7), vy: rand(-1, -0.2), life: 0, max: rand(46, 84), size: rand(1.1, 2.6), hue: rand(250, 320) });
}
if (sparks.length > 100) sparks.splice(0, sparks.length - 100);
ctx.clearRect(0, 0, w, h);
const t = performance.now() / 1000;
const breathe = 1 + Math.sin(t * 1.7) * 0.07;
// large soft glow
const R = 210 * breathe;
let g = ctx.createRadialGradient(ex, ey, 0, ex, ey, R);
g.addColorStop(0, `rgba(122,86,255,${0.17 * present})`);
g.addColorStop(0.4, `rgba(150,112,255,${0.08 * present})`);
g.addColorStop(1, "rgba(150,112,255,0)");
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(ex, ey, R, 0, 7); ctx.fill();
// inner lavender core
g = ctx.createRadialGradient(ex, ey, 0, ex, ey, 95);
g.addColorStop(0, `rgba(196,158,255,${0.16 * present})`);
g.addColorStop(1, "rgba(196,158,255,0)");
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(ex, ey, 95, 0, 7); ctx.fill();
// trail
for (let i = 0; i < trail.length; i++) {
const p = trail[i], a = i / trail.length, rr = 30 * a;
g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, rr);
g.addColorStop(0, `rgba(140,100,255,${0.06 * a * present})`);
g.addColorStop(1, "rgba(140,100,255,0)");
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(p.x, p.y, rr, 0, 7); ctx.fill();
}
// bright core dot at real cursor
g = ctx.createRadialGradient(mx, my, 0, mx, my, 15);
g.addColorStop(0, `rgba(83,58,253,${0.5 * present})`);
g.addColorStop(1, "rgba(83,58,253,0)");
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(mx, my, 15, 0, 7); ctx.fill();
// sparkles
for (let i = sparks.length - 1; i >= 0; i--) {
const s = sparks[i]; s.life++; s.x += s.vx; s.y += s.vy; s.vy += 0.012;
const lt = s.life / s.max;
if (lt >= 1) { sparks.splice(i, 1); continue; }
const a = Math.sin(lt * Math.PI) * present;
g = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.size * 3.4);
g.addColorStop(0, `hsla(${s.hue},100%,82%,${0.85 * a})`);
g.addColorStop(1, `hsla(${s.hue},100%,82%,0)`);
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(s.x, s.y, s.size * 3.4, 0, 7); ctx.fill();
ctx.fillStyle = `hsla(${s.hue},100%,94%,${a})`;
ctx.beginPath(); ctx.arc(s.x, s.y, s.size * 0.7, 0, 7); ctx.fill();
}
raf = requestAnimationFrame(loop);
};
loop();
return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", resize); unsub(); };
}, []);
return ;
}
/* ---- Magnetic — element eases toward the cursor while hovered ---- */
function Magnetic({ children, strength = 0.35, className = "", style = {} }) {
const ref = React.useRef(null);
React.useEffect(() => {
const el = ref.current;
if (!el || _fxReduce()) return;
const onMove = (e) => {
const r = el.getBoundingClientRect();
const dx = e.clientX - (r.left + r.width / 2);
const dy = e.clientY - (r.top + r.height / 2);
el.style.transform = `translate(${(dx * strength).toFixed(2)}px, ${(dy * strength).toFixed(2)}px)`;
};
const onLeave = () => { el.style.transform = "translate(0,0)"; };
el.addEventListener("mousemove", onMove);
el.addEventListener("mouseleave", onLeave);
return () => { el.removeEventListener("mousemove", onMove); el.removeEventListener("mouseleave", onLeave); };
}, [strength]);
return {children};
}
/* ---- useSpotlight — sets --sx/--sy/--sop on an element for a cursor spotlight ---- */
function useSpotlight() {
const ref = React.useRef(null);
React.useEffect(() => {
const el = ref.current;
if (!el || _fxReduce()) return;
const onMove = (e) => {
const r = el.getBoundingClientRect();
el.style.setProperty("--sx", (e.clientX - r.left) + "px");
el.style.setProperty("--sy", (e.clientY - r.top) + "px");
el.style.setProperty("--sop", "1");
};
const onLeave = () => el.style.setProperty("--sop", "0");
el.addEventListener("mousemove", onMove);
el.addEventListener("mouseleave", onLeave);
return () => { el.removeEventListener("mousemove", onMove); el.removeEventListener("mouseleave", onLeave); };
}, []);
return ref;
}
/* ---- useTilt — 3D tilt toward pointer over an element ---- */
function useTilt(max = 8, scale = 1) {
const ref = React.useRef(null);
React.useEffect(() => {
const el = ref.current;
if (!el || _fxReduce()) 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 * max).toFixed(2)}deg) rotateY(${(px * max).toFixed(2)}deg) scale(${scale})`;
};
const onLeave = () => { el.style.transform = "perspective(900px) rotateX(0) rotateY(0) scale(1)"; };
el.addEventListener("mousemove", onMove);
el.addEventListener("mouseleave", onLeave);
return () => { el.removeEventListener("mousemove", onMove); el.removeEventListener("mouseleave", onLeave); };
}, [max, scale]);
return ref;
}
/* ---- useParallax — eased translate from pointer position ---- */
function useParallax(depth = 18) {
const ref = React.useRef(null);
React.useEffect(() => {
if (_fxReduce()) return;
let raf, tx = 0, ty = 0, cx = 0, cy = 0;
const unsub = FxPointer.subscribe(({ nx, ny }) => { tx = -nx * depth; ty = -ny * depth; });
const loop = () => {
cx += (tx - cx) * 0.07; cy += (ty - cy) * 0.07;
if (ref.current) ref.current.style.transform = `translate3d(${cx.toFixed(2)}px, ${cy.toFixed(2)}px, 0)`;
raf = requestAnimationFrame(loop);
};
loop();
return () => { cancelAnimationFrame(raf); unsub(); };
}, [depth]);
return ref;
}
/* ---- useCountdown — ms remaining split into d/h/m/s ---- */
function useCountdown(targetMs) {
const [now, setNow] = React.useState(Date.now());
React.useEffect(() => {
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
}, []);
let diff = Math.max(0, targetMs - now);
const d = Math.floor(diff / 864e5); diff -= d * 864e5;
const h = Math.floor(diff / 36e5); diff -= h * 36e5;
const m = Math.floor(diff / 6e4); diff -= m * 6e4;
const s = Math.floor(diff / 1e3);
const pad = (n) => String(n).padStart(2, "0");
return { d: pad(d), h: pad(h), m: pad(m), s: pad(s) };
}
Object.assign(window, { FxPointer, Reveal, CountUp, CursorGlow, Magnetic, useSpotlight, useTilt, useParallax, useCountdown, _fxReduce });
/* cache-bust r2 */