// shared.jsx — small reusable primitives for CAJE Collective
const { useEffect, useRef, useState, useMemo, useCallback } = React;
/* ----------------- Responsive hook ----------------- */
// Returns true when viewport width is at/below the breakpoint (default 768 = mobile).
function useIsMobile(bp = 768) {
const [m, setM] = useState(() => typeof window !== 'undefined' && window.innerWidth <= bp);
useEffect(() => {
const on = () => setM(window.innerWidth <= bp);
on();
window.addEventListener('resize', on);
return () => window.removeEventListener('resize', on);
}, [bp]);
return m;
}
/* ----------------- Reveal on scroll ----------------- */
function useReveal() {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
e.target.classList.add('in');
io.unobserve(e.target);
}
});
}, { threshold: 0.12, rootMargin: '0px 0px -60px 0px' });
el.querySelectorAll('.reveal').forEach(n => io.observe(n));
return () => io.disconnect();
}, []);
return ref;
}
/* ----------------- CAJE Wordmark ----------------- */
function Wordmark({ size = 'md', inverse = false, mono = false }) {
const sizes = {
sm: { caje: 16, sub: 7.5, gap: 6, letter: 0.42 },
md: { caje: 22, sub: 8.5, gap: 8, letter: 0.5 },
lg: { caje: 42, sub: 11, gap: 14, letter: 0.6 },
xl: { caje: 80, sub: 14, gap: 22, letter: 0.7 },
};
const s = sizes[size];
const color = inverse ? '#F5F0E6' : '#111111';
const sub = inverse ? 'rgba(245,240,230,0.65)' : 'rgba(17,17,17,0.55)';
const ruleColor = mono ? color : '#B89A79';
return (
);
}
/* ----------------- Monogram (circular C) ----------------- */
function Monogram({ size = 54, inverse = false }) {
const fg = inverse ? '#F5F0E6' : '#111111';
const bronze = '#B89A79';
return (
);
}
/* ----------------- CTA button ----------------- */
function CTA({ children, onClick, dark = false, small = false, href }) {
const style = {
padding: small ? '14px 24px' : '18px 32px',
fontSize: small ? 10 : 11,
background: dark ? '#111' : 'transparent',
color: dark ? '#F5F0E6' : '#111',
borderColor: dark ? '#111' : '#B89A79',
};
const content = (
<>
{children}
>
);
if (href) {
return {content};
}
return ;
}
/* ----------------- Eyebrow (numbered section header) ----------------- */
function Eyebrow({ num, label, align = 'left' }) {
return (
{num && (
{num}
)}
{label}
);
}
/* ----------------- Section headline (serif italic) ----------------- */
function Headline({ children, size = 'lg', as = 'h2', style = {} }) {
const sizes = {
sm: 38, md: 56, lg: 76, xl: 104, xxl: 140,
};
const Tag = as;
return (
{children}
);
}
/* ----------------- Image with Ken Burns ----------------- */
function KBImage({ src, alt, placeholder, ratio = '4/5', onClick, overlay = 0 }) {
return (
{src ? (

) : (
{placeholder || alt || 'image placeholder'}
)}
{overlay > 0 && (
)}
);
}
/* ----------------- Bronze cursor glow ----------------- */
function CursorGlow({ enabled = true }) {
const ref = useRef(null);
useEffect(() => {
if (!enabled) return;
const el = ref.current;
let raf;
let tx = -200, ty = -200, x = -200, y = -200;
const onMove = (e) => { tx = e.clientX; ty = e.clientY; if (el) el.classList.add('on'); };
const onLeave = () => { if (el) el.classList.remove('on'); };
const tick = () => {
x += (tx - x) * 0.18;
y += (ty - y) * 0.18;
if (el) el.style.transform = `translate(${x}px, ${y}px) translate(-50%, -50%)`;
raf = requestAnimationFrame(tick);
};
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseleave', onLeave);
raf = requestAnimationFrame(tick);
return () => {
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseleave', onLeave);
cancelAnimationFrame(raf);
};
}, [enabled]);
if (!enabled) return null;
return ;
}
/* ----------------- Lightbox ----------------- */
function Lightbox({ images, index, onClose, onNav }) {
useEffect(() => {
if (index == null) return;
const onKey = (e) => {
if (e.key === 'Escape') onClose();
if (e.key === 'ArrowRight') onNav(1);
if (e.key === 'ArrowLeft') onNav(-1);
};
window.addEventListener('keydown', onKey);
document.body.style.overflow = 'hidden';
return () => {
window.removeEventListener('keydown', onKey);
document.body.style.overflow = '';
};
}, [index, onClose, onNav]);
if (index == null) return null;
const img = images[index];
const isMobile = useIsMobile();
return (
{String(index + 1).padStart(2, '0')} / {String(images.length).padStart(2, '0')}
{img.caption}
{img.src ? (

) : (
{img.caption}
)}
);
}
function navBtn(side, isMobile) {
return {
position: 'absolute',
[side]: isMobile ? '50%' : 30,
bottom: isMobile ? 20 : 'auto',
top: isMobile ? 'auto' : '50%',
transform: isMobile
? `translateX(${side === 'left' ? '-70px' : '10px'})`
: 'translateY(-50%)',
width: 54, height: 54, borderRadius: '50%',
background: isMobile ? 'rgba(17,17,17,0.6)' : 'transparent',
border: '1px solid rgba(184,154,121,0.5)',
color: 'rgba(245,240,230,0.85)',
fontSize: 28, fontFamily: 'EB Garamond, serif',
cursor: 'pointer',
transition: 'all 0.4s',
};
}
/* ----------------- Marquee line ----------------- */
function TickerStrip({ items }) {
return (
{[...items, ...items].map((t, i) => (
{t}
))}
);
}
Object.assign(window, {
useReveal, useIsMobile, Wordmark, Monogram, CTA, Eyebrow, Headline, KBImage, CursorGlow, Lightbox, TickerStrip,
});