// Shared utilities, orbit mark, cursor, reveal, etc. // Exports globally: OrbitMark, OrbitBig, Cursor, useReveal, MagneticLink, Arrow, useScrolled const { useState, useEffect, useRef, useCallback } = React; // ---- The orbit mark (ring + core + spark) -------------------- function OrbitMark({ size = 32, color = "var(--oz-purple-500)", spark = "var(--oz-gold-400)", animated = false, className = "" }) { return ( ); } // ---- Big layered orbit used in hero -------------------------- function OrbitBig() { return ( ); } // ---- Per-section orbit (mobile only) ------------------------ function SectionOrbit({ variant = "services" }) { const common = { className: "section__orbit", "aria-hidden": "true", viewBox: "0 0 64 64", width: "56", height: "56" }; if (variant === "services") { return ( ); } if (variant === "process") { return ( ); } if (variant === "about") { return ( ); } // contact return ( ); } // ---- Custom cursor ------------------------------------------- function Cursor({ enabled }) { const dotRef = useRef(null); const ringRef = useRef(null); const pos = useRef({ x: -100, y: -100 }); const ringPos = useRef({ x: -100, y: -100 }); const raf = useRef(null); const isTouch = typeof window !== 'undefined' && ( window.matchMedia('(hover: none) and (pointer: coarse)').matches || 'ontouchstart' in window ); // Touch devices: paint a gold trail that follows the finger, each point // fading independently — first-touched fades first, lift-point fades last. useEffect(() => { if (!enabled || !isTouch) return; document.body.classList.remove('cursor-custom', 'cursor-hot', 'cursor-dark'); let lastSpawn = 0; let lastX = 0; let lastY = 0; const spawn = (x, y) => { const mark = document.createElement('div'); mark.className = 'tap-mark'; mark.style.left = x + 'px'; mark.style.top = y + 'px'; document.body.appendChild(mark); setTimeout(() => mark.remove(), 1400); }; const onStart = (e) => { const t = e.touches && e.touches[0]; if (!t) return; lastX = t.clientX; lastY = t.clientY; lastSpawn = Date.now(); spawn(lastX, lastY); }; const onMove = (e) => { const t = e.touches && e.touches[0]; if (!t) return; const now = Date.now(); const dx = t.clientX - lastX; const dy = t.clientY - lastY; const dist = Math.sqrt(dx * dx + dy * dy); if (now - lastSpawn < 22 && dist < 14) return; lastX = t.clientX; lastY = t.clientY; lastSpawn = now; spawn(lastX, lastY); }; const onEnd = (e) => { const t = e.changedTouches && e.changedTouches[0]; if (!t) return; spawn(t.clientX, t.clientY); }; window.addEventListener('touchstart', onStart, { passive: true }); window.addEventListener('touchmove', onMove, { passive: true }); window.addEventListener('touchend', onEnd, { passive: true }); return () => { window.removeEventListener('touchstart', onStart); window.removeEventListener('touchmove', onMove); window.removeEventListener('touchend', onEnd); }; }, [enabled, isTouch]); useEffect(() => { if (!enabled || isTouch) { document.body.classList.remove('cursor-custom', 'cursor-hot', 'cursor-dark'); return; } document.body.classList.add('cursor-custom'); 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) translate(-50%, -50%)`; } }; const onOver = (e) => { const t = e.target.closest('[data-hover], a, button, .service-row, .hero__headline, .cta'); if (t) document.body.classList.add('cursor-hot'); else document.body.classList.remove('cursor-hot'); }; const tick = () => { ringPos.current.x += (pos.current.x - ringPos.current.x) * 0.18; ringPos.current.y += (pos.current.y - ringPos.current.y) * 0.18; if (ringRef.current) { ringRef.current.style.transform = `translate(${ringPos.current.x}px, ${ringPos.current.y}px) translate(-50%, -50%)`; } // detect element under the ring and tint accordingly raf.current = requestAnimationFrame(tick); }; const onScroll = () => { const y = ringPos.current.y || 0; const el = document.elementFromPoint(ringPos.current.x || 0, y); if (!el) return; const darkSection = el.closest('.section--dark, .section--purple, .hero, .footer, .marquee-section'); if (darkSection) document.body.classList.add('cursor-dark'); else document.body.classList.remove('cursor-dark'); }; window.addEventListener('mousemove', onMove); window.addEventListener('mouseover', onOver); window.addEventListener('mousemove', onScroll); window.addEventListener('scroll', onScroll, { passive: true }); raf.current = requestAnimationFrame(tick); return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseover', onOver); window.removeEventListener('mousemove', onScroll); window.removeEventListener('scroll', onScroll); cancelAnimationFrame(raf.current); document.body.classList.remove('cursor-custom', 'cursor-hot', 'cursor-dark'); }; }, [enabled]); if (!enabled || isTouch) return null; return ( <>
> ); } // ---- Magnetic wrapper ----------------------------------------- function Magnetic({ children, strength = 0.3, className = "" }) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; const onMove = (e) => { const rect = el.getBoundingClientRect(); const x = e.clientX - rect.left - rect.width / 2; const y = e.clientY - rect.top - rect.height / 2; el.style.transform = `translate(${x * strength}px, ${y * strength}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 ----------------------------------------- function useRevealRoot() { useEffect(() => { const els = document.querySelectorAll('.reveal'); const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('is-in'); io.unobserve(e.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -10% 0px' }); els.forEach(el => io.observe(el)); return () => io.disconnect(); }, []); } // ---- Sticky-nav scroll state --------------------------------- function useScrolled(threshold = 40) { const [s, setS] = useState(false); useEffect(() => { const onScroll = () => setS(window.scrollY > threshold); onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, [threshold]); return s; } // ---- Track which section the viewport top is inside ---------- function useActiveSection() { const [dark, setDark] = useState(true); // hero starts dark useEffect(() => { const check = () => { const y = 80; const el = document.elementFromPoint(window.innerWidth / 2, y); if (!el) return; const darkEl = el.closest('.section--dark, .section--purple, .hero, .footer'); setDark(!!darkEl); }; check(); window.addEventListener('scroll', check, { passive: true }); window.addEventListener('resize', check); return () => { window.removeEventListener('scroll', check); window.removeEventListener('resize', check); }; }, []); return dark; } // ---- Icons ---------------------------------------------------- function ArrowUpRight({ size = 18 }) { return ( ); } function ArrowRight({ size = 18 }) { return ( ); } function Check({ size = 24 }) { return ( ); } function Spark({ size = 18 }) { return ( ); } Object.assign(window, { OrbitMark, OrbitBig, SectionOrbit, Cursor, Magnetic, useRevealRoot, useScrolled, useActiveSection, ArrowUpRight, ArrowRight, Check, Spark, });