/* ui.jsx — shared primitives: scroll-reveal, media hook, small helpers */
const { useState, useEffect, useRef, useCallback } = React;
/* Reveal-on-scroll wrapper */
function Reveal({ children, className = '', delay = 0, tag = 'div', ...rest }) {
const ref = useRef(null);
const [seen, setSeen] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => { if (e.isIntersecting) { setSeen(true); io.disconnect(); } });
}, { threshold: 0.18 });
io.observe(el);
return () => io.disconnect();
}, []);
const Tag = tag;
const d = delay ? ` d${delay}` : '';
return {children};
}
/* live media list, re-reads on the global 'r50:media' event */
function useMediaList(filter) {
const [items, setItems] = useState([]);
const load = useCallback(async () => {
const all = await R50.getMedia();
setItems(filter ? all.filter(filter) : all);
}, [filter]);
useEffect(() => {
load();
const h = () => load();
window.addEventListener('r50:media', h);
return () => window.removeEventListener('r50:media', h);
}, [load]);
return items;
}
function bumpMedia() { window.dispatchEvent(new Event('r50:media')); }
/* read a file from an or drop, with basic guard rails */
function pickFile(onFile, { accept } = {}) {
const input = document.createElement('input');
input.type = 'file';
if (accept) input.accept = accept;
input.onchange = () => { if (input.files && input.files[0]) onFile(input.files[0]); };
input.click();
}
const SECTION_LINKS = [
['Invitation', 'invitation'],
['Details', 'details'],
['RSVP', 'rsvp'],
['Guestbook', 'guestbook'],
['Gallery', 'gallery'],
];
Object.assign(window, { Reveal, useMediaList, bumpMedia, pickFile, SECTION_LINKS });