/* App.jsx — main composition + GSAP scroll choreography + tweaks */
const { useEffect, useState, useRef } = React;
// Default tweak values — wrapped in EDITMODE markers so host can persist edits
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"animationIntensity": 1.0,
"showCharacterChips": true,
"comicSansOnPepeX": true,
"showVideos": true
}/*EDITMODE-END*/;
function App() {
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
const [active, setActive] = useState('top');
// Scrollspy
useEffect(() => {
const ids = ['idea', 'origins', 'timeline', 'fund', 'collectibles', 'people', 'characters', 'collabs'];
const onScroll = () => {
let current = 'top';
for (const id of ids) {
const el = document.getElementById(id);
if (!el) continue;
const top = el.getBoundingClientRect().top;
if (top < 160) current = id;
}
setActive(current);
};
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
return () => window.removeEventListener('scroll', onScroll);
}, []);
// GSAP scroll choreography
useEffect(() => {
if (!window.gsap) return;
const gsap = window.gsap;
const ScrollTrigger = window.ScrollTrigger;
if (ScrollTrigger) gsap.registerPlugin(ScrollTrigger);
const intensity = t.animationIntensity || 1;
const triggers = [];
// Hero clouds — parallax X+Y at different speeds
document.querySelectorAll('.hero-cloud').forEach((c) => {
const speed = parseFloat(c.dataset.speed || '0.4');
const tr = gsap.to(c, {
yPercent: -150 * speed * intensity,
xPercent: 70 * speed * intensity,
ease: 'none',
scrollTrigger: {
trigger: '.hero',
start: 'top top',
end: 'bottom top',
scrub: true,
}
});
triggers.push(tr.scrollTrigger);
});
// Wordmark inner — fade up on scroll in
const wm = document.querySelector('[data-wordmark] .wordmark-inner');
if (wm) {
const tr = gsap.fromTo(wm,
{ y: 60, opacity: 0 },
{ y: 0, opacity: 1, duration: 1.4, ease: 'power3.out',
scrollTrigger: { trigger: '[data-wordmark]', start: 'top 70%' }});
triggers.push(tr.scrollTrigger);
}
// Idea characters — enter from sides
document.querySelectorAll('[data-idea-char]').forEach((el, i) => {
const fromX = el.dataset.side === 'L' ? -220 : 220;
const tr = gsap.fromTo(el,
{ x: fromX * intensity, opacity: 0, scale: 0.6 },
{
x: 0, opacity: 1, scale: 1,
duration: 1, ease: 'power3.out', delay: i * 0.08,
scrollTrigger: { trigger: '.idea-sec', start: 'top 65%' }
});
triggers.push(tr.scrollTrigger);
});
// Origins works — reveal in
document.querySelectorAll('.work').forEach((el, i) => {
const tr = gsap.fromTo(el,
{ y: 30, opacity: 0 },
{ y: 0, opacity: 1, duration: 0.7, delay: i * 0.05, ease: 'power3.out',
scrollTrigger: { trigger: '.origins-works', start: 'top 80%' }});
triggers.push(tr.scrollTrigger);
});
// Timeline thumbs in
document.querySelectorAll('.tl-thumb').forEach((el, i) => {
const tr = gsap.fromTo(el,
{ y: el.classList.contains('up') ? -96 : 96, opacity: 0 },
{ y: el.classList.contains('up') ? -126 : 126, opacity: 1,
duration: 1, delay: i * 0.12, ease: 'power3.out',
scrollTrigger: { trigger: '.timeline-rail', start: 'top 70%' }});
triggers.push(tr.scrollTrigger);
});
// ---- Bento scrub-zoom: pinned. center cell zooms to full screen, becomes "The Fund" header ----
const bento = document.querySelector('[data-bento]');
const center = document.querySelector('[data-bento-center]');
const pin = document.querySelector('[data-bento-pin]');
if (bento && center && pin) {
const others = bento.querySelectorAll('.bento-cell:not(.cc)');
const tl = gsap.timeline({
scrollTrigger: {
trigger: pin,
start: 'top top',
end: '+=150%',
scrub: 1,
pin: true,
pinSpacing: true,
anticipatePin: 1,
}
});
// First: hold, then zoom center while fading others
tl.to(center, { scale: 6.5, duration: 1, ease: 'power2.inOut' }, 0)
.to(others, { opacity: 0, duration: 0.45, stagger: 0.02 }, 0);
triggers.push(tl.scrollTrigger);
}
// Horizontal type pin + scroll
const horiz = document.querySelector('[data-horiz]');
const track = document.querySelector('[data-horiz-track]');
if (horiz && track) {
const tr = gsap.to(track, {
x: () => -(track.scrollWidth - window.innerWidth + 100),
ease: 'none',
scrollTrigger: {
trigger: horiz,
start: 'top top',
end: () => '+=' + (track.scrollWidth - window.innerWidth + 100),
scrub: 0.8,
pin: true,
invalidateOnRefresh: true,
}
});
triggers.push(tr.scrollTrigger);
}
// Squad cards stagger in
document.querySelectorAll('[data-squad-card]').forEach((card, i) => {
const tr = gsap.fromTo(card,
{ y: 60, opacity: 0 },
{ y: 0, opacity: 1, duration: 0.7, delay: i * 0.1, ease: 'power3.out',
scrollTrigger: { trigger: '.squad', start: 'top 75%' }});
triggers.push(tr.scrollTrigger);
});
// Collabs final reveal
const ci = document.querySelector('[data-collabs-inner]');
if (ci) {
const tr = gsap.fromTo(ci,
{ y: 60, opacity: 0, scale: 0.96 },
{ y: 0, opacity: 1, scale: 1, duration: 1.4, ease: 'power3.out',
scrollTrigger: { trigger: '.collabs', start: 'top 70%' }});
triggers.push(tr.scrollTrigger);
}
return () => {
triggers.forEach(tr => tr && tr.kill && tr.kill());
};
}, [t.animationIntensity]);
// Character chips toggle
useEffect(() => {
const el = document.querySelector('.idea-chars');
if (el) el.style.display = t.showCharacterChips ? '' : 'none';
}, [t.showCharacterChips]);
// Comic Sans toggle on Pepe X
useEffect(() => {
const el = document.querySelector('.char-pepex');
if (el) el.classList.toggle('no-comic', !t.comicSansOnPepeX);
}, [t.comicSansOnPepeX]);
// Video bg toggle (saves battery)
useEffect(() => {
document.querySelectorAll('section video').forEach(v => {
if (t.showVideos) {
v.play().catch(() => {});
v.style.display = '';
} else {
v.pause();
v.style.display = 'none';
}
});
}, [t.showVideos]);
return (
setTweak('animationIntensity', v)} />
setTweak('showVideos', v)} />
setTweak('showCharacterChips', v)} />
setTweak('comicSansOnPepeX', v)} />
);
}
const tweakLinkStyle = {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
padding: '7px 10px', borderRadius: 8, fontSize: 11, fontWeight: 600,
background: 'rgba(0,0,0,0.06)', color: 'inherit', textDecoration: 'none',
};
ReactDOM.createRoot(document.getElementById('root')).render();