/* app.jsx — entry reveal, hero, nav, music, overlays, composition */
const { useState: useS, useEffect: useE, useRef: useR } = React;
/* ---- fun looping piano (royalty-free, synthesized — no audio file) ---------
Bright, celebratory I–V–vi–IV arpeggio with a struck-string envelope + echo. */
function makeAmbient() {
let ctx, master, lp, delay, fb, wet, timer, on = false, step = 0;
const CH = (b, a, c, d) => ({ bass: b, arp: [a, c, d, c] });
const prog = [
CH(130.81, 261.63, 329.63, 392.00), // C
CH(98.00, 246.94, 293.66, 392.00), // G
CH(110.00, 261.63, 329.63, 440.00), // Am
CH(87.31, 261.63, 349.23, 440.00), // F
];
const STEP = 0.38;
function voice(freq, t, dur, peak) {
const g = ctx.createGain();
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(peak, t + 0.01);
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
g.connect(lp);
const o1 = ctx.createOscillator(); o1.type = 'triangle'; o1.frequency.value = freq;
const o2 = ctx.createOscillator(); o2.type = 'sine'; o2.frequency.value = freq * 2;
const g2 = ctx.createGain(); g2.gain.value = 0.28; o2.connect(g2); g2.connect(g);
o1.connect(g);
o1.start(t); o2.start(t); o1.stop(t + dur + 0.1); o2.stop(t + dur + 0.1);
}
function tick() {
if (!on) return;
const t = ctx.currentTime + 0.06;
const chord = prog[Math.floor(step / 4) % prog.length];
const idx = step % 4;
if (idx === 0) voice(chord.bass, t, 2.2, 0.14);
voice(chord.arp[idx], t, 1.8, 0.18);
step++;
timer = setTimeout(tick, STEP * 1000);
}
return {
toggle() {
if (!on) {
ctx = ctx || new (window.AudioContext || window.webkitAudioContext)();
if (ctx.state === 'suspended') ctx.resume();
if (!master) {
master = ctx.createGain(); master.gain.value = 0; master.connect(ctx.destination);
lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 3200; lp.connect(master);
delay = ctx.createDelay(); delay.delayTime.value = 0.3;
fb = ctx.createGain(); fb.gain.value = 0.24;
wet = ctx.createGain(); wet.gain.value = 0.22;
lp.connect(delay); delay.connect(fb); fb.connect(delay); delay.connect(wet); wet.connect(master);
}
on = true; step = 0;
master.gain.cancelScheduledValues(ctx.currentTime);
master.gain.setTargetAtTime(0.85, ctx.currentTime, 0.8);
tick();
} else {
on = false;
clearTimeout(timer);
if (master) master.gain.setTargetAtTime(0, ctx.currentTime, 0.5);
}
return on;
},
get on() { return on; },
};
}
const ambient = makeAmbient();
function Entry({ onEnter }) {
const [breaking, setBreaking] = useS(false);
const sparks = React.useMemo(() => Array.from({ length: 22 }, () => ({
l: Math.random() * 100, t: Math.random() * 100, d: Math.random() * 3, s: 2 + Math.random() * 3, sz: 2 + Math.random() * 3,
})), []);
const open = () => {
if (breaking) return;
setBreaking(true);
try { ambient.toggle(); } catch (e) {}
setTimeout(onEnter, 950);
};
return (
{sparks.map((s, i) => (
))}
You're invited to celebrate
Rajshree turns fifty
R
Tap the seal to open
);
}
function Hero() {
const heroRef = useR(null);
const cardRef = useR(null);
useE(() => {
const cleanDust = window.R50FX ? window.R50FX.heroDust(heroRef.current) : null;
let cleanTilt = null;
if (cardRef.current && window.R50FX) {
// entrance, then enable parallax tilt
const a = cardRef.current.animate(
[{ opacity: 0, transform: 'translateY(42px) scale(.96)' }, { opacity: 1, transform: 'none' }],
{ duration: 1300, delay: 250, easing: 'cubic-bezier(.2,.7,.2,1)', fill: 'backwards' }
);
a.onfinish = () => { cleanTilt = window.R50FX.tilt(cardRef.current, 6); };
}
return () => { cleanDust && cleanDust(); cleanTilt && cleanTilt(); };
}, []);
return (
Rajshree
turns
50
An afternoon of love, laughter, and five wonderful decades worth celebrating.
Scroll
);
}
function Nav({ onMusic, musicOn }) {
const [solid, setSolid] = useS(false);
const [open, setOpen] = useS(false);
useE(() => {
const h = () => setSolid(window.scrollY > 80);
window.addEventListener('scroll', h); h();
return () => window.removeEventListener('scroll', h);
}, []);
return (
);
}
function Lightbox({ item, onClose }) {
if (!item) return null;
return (
{item.type === 'image'
?

e.stopPropagation()} />
:
);
}
function Slideshow({ list, onClose }) {
const [i, setI] = useS(0);
useE(() => {
if (!list) return;
const id = setInterval(() => setI((x) => (x + 1) % list.length), 4200);
const key = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', key);
return () => { clearInterval(id); window.removeEventListener('keydown', key); };
}, [list]);
if (!list) return null;
const m = list[i];
return (
{i + 1} / {list.length}

{(m.name || m.caption) &&
{m.caption || ''}{m.name ? '— ' + m.name : ''}
}
);
}
function App() {
const [entered, setEntered] = useS(false);
const [lightItem, setLightItem] = useS(null);
const [slides, setSlides] = useS(null);
const [toast, setToast] = useS('');
const [musicOn, setMusicOn] = useS(false);
const toastTimer = useR(null);
useE(() => {
window.R50UI = {
toast: (msg) => {
setToast(msg);
clearTimeout(toastTimer.current);
toastTimer.current = setTimeout(() => setToast(''), 2600);
},
lightbox: (item) => setLightItem(item),
slideshow: (list) => { if (list && list.length) setSlides(list); },
};
}, []);
useE(() => { document.body.classList.toggle('locked', !entered); }, [entered]);
// scroll progress ribbon
useE(() => {
const bar = document.getElementById('progress');
const h = () => {
const max = document.documentElement.scrollHeight - window.innerHeight;
const p = max > 0 ? window.scrollY / max : 0;
if (bar) bar.style.transform = 'scaleX(' + p + ')';
};
window.addEventListener('scroll', h, { passive: true }); h();
return () => window.removeEventListener('scroll', h);
}, [entered]);
const toggleMusic = () => { try { setMusicOn(ambient.toggle()); } catch (e) {} };
return (
<>
{entered && }
{!entered && { setEntered(true); setMusicOn(ambient.on); }} />}
setLightItem(null)} />
{slides && setSlides(null)} />}
{toast}
>
);
}
ReactDOM.createRoot(document.getElementById('root')).render();