// Parallax, scroll-reveal, and atmospheric imagery helpers
// Uses Unsplash Source (no API key needed) for nature backgrounds.
// All images are drawn into with decoding=async & loading=lazy.
// Parallax uses CSS custom properties driven by requestAnimationFrame — GPU-cheap.
const { useState, useEffect, useRef } = React;
// ─── Nature imagery bank (Unsplash — royalty-free, hotlinkable)
// Each slot is a curated URL; swap for real brand photography later.
const IMAGERY = {
hero: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=1800&q=80&auto=format&fit=crop', // misty forest
hero2: 'https://images.unsplash.com/photo-1511497584788-876760111969?w=1800&q=80&auto=format&fit=crop', // fog mountains
programs: 'https://images.unsplash.com/photo-1465146344425-f00d5f5c8f07?w=1800&q=80&auto=format&fit=crop', // fern
process: 'https://images.unsplash.com/photo-1518495973542-4542c06a5843?w=1800&q=80&auto=format&fit=crop', // sun through trees
quiz: 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=1800&q=80&auto=format&fit=crop', // mountain lake
science: 'https://images.unsplash.com/photo-1501854140801-50d01698950b?w=1800&q=80&auto=format&fit=crop', // top-down forest
team: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1800&q=80&auto=format&fit=crop', // mountain range
testim: 'https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=1800&q=80&auto=format&fit=crop', // golden hour field
finalCta: 'https://images.unsplash.com/photo-1483728642387-6c3bdd6c93e5?w=1800&q=80&auto=format&fit=crop', // mountain dawn
programBg: {
'body': 'https://images.unsplash.com/photo-1551632811-561732d1e306?w=1400&q=80&auto=format&fit=crop', // hiker from behind with backpack on mountain trail
'gi': 'https://images.unsplash.com/photo-1518495973542-4542c06a5843?w=1400&q=80&auto=format&fit=crop', // forest light — renewal
'neuro': 'https://images.unsplash.com/photo-1419242902214-272b3f66ee7a?w=1400&q=80&auto=format&fit=crop', // milky way — clarity, cognition
'regen': 'https://images.unsplash.com/photo-1502082553048-f009c37129b9?w=1400&q=80&auto=format&fit=crop', // river rock — regeneration
'hormone': 'https://images.unsplash.com/uploads/1412533519888a485b488/bb9f9777?w=1400&q=80&auto=format&fit=crop', // man walking on forest path — balance in motion
'longevity': 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=1400&q=80&auto=format&fit=crop', // ancient lake — longevity
}
};
// ─── Global parallax driver
// Tracks scrollY once per frame and writes to CSS var --sy on .
// Then any element with data-parallax="N" uses calc(var(--sy) * N) in CSS for translate.
function useGlobalScrollVar() {
useEffect(() => {
let ticking = false;
const update = () => {
document.documentElement.style.setProperty('--sy', window.scrollY + 'px');
document.documentElement.style.setProperty('--syN', String(window.scrollY));
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(update);
ticking = true;
}
};
window.addEventListener('scroll', onScroll, { passive: true });
update();
return () => window.removeEventListener('scroll', onScroll);
}, []);
}
// ─── Scroll-reveal: fades + lifts children as they enter viewport.
// Apply className="reveal" to any element. The observer toggles .in-view.
function useScrollReveal() {
useEffect(() => {
const els = document.querySelectorAll('.reveal, .reveal-stagger > *');
if (!('IntersectionObserver' in window)) {
els.forEach(el => el.classList.add('in-view'));
return;
}
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
e.target.classList.add('in-view');
io.unobserve(e.target);
}
});
}, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
els.forEach(el => io.observe(el));
// Safety net: force-reveal anything that hasn't intersected within 300ms
// (fixes cases where elements are inside transformed ancestors that confuse IO)
const safety = setTimeout(() => {
els.forEach(el => {
const r = el.getBoundingClientRect();
if (r.top < window.innerHeight && r.bottom > 0) el.classList.add('in-view');
});
}, 300);
return () => { clearTimeout(safety); io.disconnect(); };
}, []);
}
// ─── Parallax image layer — uses section-local scroll position for smooth motion.
// speed: 0.0 = static, 0.3 = gentle, 0.6 = strong, negative = reverse.
const ParallaxImage = ({ src, alt = '', speed = 0.3, scale = 1.2, overlay = 0.6, blur = 0, className = '', children, position = 'center center' }) => {
const ref = useRef(null);
const imgRef = useRef(null);
const [offset, setOffset] = useState(0);
useEffect(() => {
const el = ref.current;
if (!el) return;
let rafId = null;
const update = () => {
const rect = el.getBoundingClientRect();
const vh = window.innerHeight;
// -1 when bottom of section is at top of viewport; 1 when top of section at bottom.
const progress = (rect.top + rect.height / 2 - vh / 2) / (vh + rect.height);
setOffset(-progress * speed * 100);
rafId = null;
};
const onScroll = () => {
if (rafId == null) rafId = window.requestAnimationFrame(update);
};
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
update();
return () => {
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onScroll);
};
}, [speed]);
return (