// TapeDeck.jsx — v2: the deck is now a calm "Now playing" strip.
// Same API as the old hi-fi deck (section, onEject, swapState, counter) so
// app.jsx is untouched. The reading progress bar tracks real page scroll —
// the tape position is how far through the side you've read.
function SpinningReel({ color, playing }) {
// small line-drawn tape glyph: two reels + a connecting window
const reel = {
width: 16, height: 16,
borderRadius: '50%',
border: '1.5px dashed var(--c-smoke)',
animation: playing ? 'reel-spin 5s linear infinite' : 'none',
flexShrink: 0,
};
return (
);
}
function TapeDeck({ section, onEject, swapState, counter }) {
const isEmpty = !section;
const isPlaying = swapState === 'idle' && !!section;
const [prog, setProg] = React.useState(0);
const [hoverEject, setHoverEject] = React.useState(false);
React.useEffect(() => {
const onScroll = () => {
const h = document.documentElement;
const max = h.scrollHeight - window.innerHeight;
setProg(max > 60 ? Math.min(1, Math.max(0, window.scrollY / max)) : 0);
};
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
// content height settles after swap
const t = setTimeout(onScroll, 700);
return () => {
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onScroll);
clearTimeout(t);
};
}, [section && section.id, swapState]);
const contentAnim =
swapState === 'ejecting' ? 'tape-eject 420ms var(--ease-snap) forwards' :
swapState === 'loading' ? 'tape-load 480ms var(--ease-out) both' :
'none';
return (
{isEmpty ? (
Nothing playing. Pick a side below.
) : (
<>
{swapState === 'loading' ? 'Loading' : swapState === 'ejecting' ? 'Ejecting' : 'Now playing'}
Side {section.side} · {section.title}
{/* reading progress = tape position */}
{Math.round(prog * 100)}%
>
)}
{!isEmpty && (
setHoverEject(true)}
onMouseLeave={() => setHoverEject(false)}
aria-label="Eject — back to the index"
style={{
appearance: 'none',
background: 'transparent',
border: `1px solid ${hoverEject ? 'var(--ink-coral)' : 'var(--c-border)'}`,
color: hoverEject ? 'var(--ink-coral)' : 'var(--c-whisper)',
fontFamily: 'var(--font-body)',
fontSize: 11.5,
fontWeight: 600,
letterSpacing: '0.1em',
textTransform: 'uppercase',
padding: '8px 14px',
cursor: 'pointer',
transition: 'color 160ms, border-color 160ms',
flexShrink: 0,
}}>
⏏ Eject
)}
);
}
window.TapeDeck = TapeDeck;