// ─── SENES MEDIA · shared components (cursor, scramble, magnetic) ───── // ─── Custom cursor with magnetic ring + label ───────────────── function Cursor() { const dotRef = React.useRef(null); const ringRef = React.useRef(null); const labelRef = React.useRef(null); const labelTextRef = React.useRef(""); const target = React.useRef({ x: 0, y: 0 }); const ring = React.useRef({ x: 0, y: 0 }); React.useEffect(() => { let raf; const onMove = (e) => { target.current = { x: e.clientX, y: e.clientY }; if (dotRef.current) { dotRef.current.style.transform = `translate(${e.clientX}px, ${e.clientY}px) translate(-50%, -50%)`; } if (labelRef.current) { labelRef.current.style.transform = `translate(${e.clientX + 18}px, ${e.clientY + 18}px)`; } }; const tick = () => { ring.current.x += (target.current.x - ring.current.x) * 0.18; ring.current.y += (target.current.y - ring.current.y) * 0.18; if (ringRef.current) { ringRef.current.style.transform = `translate(${ring.current.x}px, ${ring.current.y}px) translate(-50%, -50%)`; } raf = requestAnimationFrame(tick); }; const onOver = (e) => { const t = e.target.closest("[data-cursor]"); if (!t) { ringRef.current?.classList.remove("hover", "text", "drag"); labelRef.current?.classList.remove("show"); return; } const mode = t.getAttribute("data-cursor"); ringRef.current?.classList.remove("hover", "text", "drag"); if (mode === "text") ringRef.current?.classList.add("text"); else if (mode === "drag") ringRef.current?.classList.add("drag"); else ringRef.current?.classList.add("hover"); const label = t.getAttribute("data-cursor-label"); if (label) { labelTextRef.current = label; if (labelRef.current) labelRef.current.textContent = label; labelRef.current?.classList.add("show"); } else { labelRef.current?.classList.remove("show"); } }; window.addEventListener("mousemove", onMove); window.addEventListener("mouseover", onOver); tick(); return () => { window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseover", onOver); cancelAnimationFrame(raf); }; }, []); return (
); } // ─── Scramble text on intersect ────────────────────────────── function Scramble({ text, className = "", as: As = "span", trigger = "view", delay = 0 }) { const ref = React.useRef(null); const [out, setOut] = React.useState(text); const charsRef = React.useRef("!<>-_\\/[]{}—=+*^?#________"); const fired = React.useRef(false); const run = React.useCallback(() => { if (fired.current) return; fired.current = true; const target = text; const chars = charsRef.current; let frame = 0; const total = 24; const queue = target.split("").map((ch, i) => { const start = Math.floor(Math.random() * total * 0.4); const end = start + Math.floor(Math.random() * total * 0.6) + 6; return { from: chars[Math.floor(Math.random() * chars.length)], to: ch, start, end }; }); const step = () => { let output = ""; let done = 0; for (let i = 0; i < queue.length; i++) { const { from, to, start, end } = queue[i]; if (frame >= end) { output += to; done++; } else if (frame >= start) { if (Math.random() < 0.28) queue[i].from = chars[Math.floor(Math.random() * chars.length)]; output += `${queue[i].from}`; } else { output += to === " " ? " " : `${to}`; } } setOut(output); frame++; if (done < queue.length) requestAnimationFrame(step); else setOut(target); }; setTimeout(() => step(), delay); }, [text, delay]); React.useEffect(() => { if (trigger === "mount") { run(); return; } const el = ref.current; if (!el) return; const obs = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) run(); }); }, { threshold: 0.4 }); obs.observe(el); return () => obs.disconnect(); }, [run, trigger]); return ; } // ─── Magnetic wrapper ─────────────────────────────────────── function Magnetic({ children, strength = 0.35, className = "", ...rest }) { const ref = React.useRef(null); React.useEffect(() => { const el = ref.current; if (!el) return; const onMove = (e) => { const r = el.getBoundingClientRect(); const x = (e.clientX - r.left - r.width / 2) * strength; const y = (e.clientY - r.top - r.height / 2) * strength; el.style.transform = `translate(${x}px, ${y}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}
; } // ─── Reveal on scroll (re-triggers when re-entering view) ── function Reveal({ children, delay = 0, className = "" }) { const ref = React.useRef(null); React.useEffect(() => { const el = ref.current; if (!el) return; const obs = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { setTimeout(() => el.classList.add("in"), delay); } else { // re-arm: only if user has scrolled clearly past it if (e.boundingClientRect.top > window.innerHeight * 0.9 || e.boundingClientRect.bottom < -50) { el.classList.remove("in"); } } }); }, { threshold: 0.15, rootMargin: "0px 0px -10% 0px" }); obs.observe(el); return () => obs.disconnect(); }, [delay]); return
{children}
; } // ─── SENES Logo mark (red 4-petal swirl) ───────────────────── function LogoMark({ size = 28, color = "var(--red)" }) { return ( SENES ); } // ─── Placeholder image card ───────────────────────────────── function Placeholder({ label = "image", color = "#1a1a1a", accent = "#666", aspect = "16/10", note = "", style = {} }) { return (
{label}
{note ?
{note}
: null}
); } // ─── Arrow icon ─────────────────────────────────────────── function Arrow({ size = 14 }) { return ( ); } // ─── Marquee ────────────────────────────────────────────── function Marquee({ children, speed = 40, direction = "left" }) { return (
{children}{children}{children}
); } Object.assign(window, { Cursor, Scramble, Magnetic, Reveal, LogoMark, Placeholder, Arrow, Marquee });