const { useState, useEffect, useRef, useCallback } = React;
// ============================================================
// SONG DEFINITIONS
//
// Procedural synth songs are the built-in fallback. If
// audio/songs.json exists (generated by tools/probe-songs.py),
// those MP3-backed songs replace the builtins at startup.
// ============================================================
const BUILTIN_SONGS = [
{ title: "Bureaucratic Sunrise", artist: "The Paywalls", duration: 90, bpm: 120,
gen: (s,t) => { ["C4","E4","G4","B4","C5","B4","G4","E4"].forEach((n,i) => s.triggerAttackRelease(n,"8n",t+i*0.25)); },
wave: "triangle", color: "#e74c3c" },
{ title: "Terms of Sorrow", artist: "Subscription Model", duration: 105, bpm: 72,
gen: (s,t) => { ["A3","C4","E4","A4","G4","E4","D4","C4"].forEach((n,i) => s.triggerAttackRelease(n,"4n",t+i*0.5)); },
wave: "sine", color: "#9b59b6" },
{ title: "Micro Transaction Blues", artist: "Nickel & Dime", duration: 75, bpm: 140,
gen: (s,t) => { ["E3","G3","A3","B3","D4","B3","A3","G3","E3","D3","E3","G3"].forEach((n,i) => s.triggerAttackRelease(n,"16n",t+i*0.18)); },
wave: "sawtooth", color: "#f39c12" },
{ title: "404 Feeling Not Found", artist: "Errorcode", duration: 80, bpm: 100,
gen: (s,t) => { ["D4","F#4","A4","D5","C#5","A4","F#4","D4","C#4","D4"].forEach((n,i) => s.triggerAttackRelease(n,"8n.",t+i*0.35)); },
wave: "square", color: "#3498db" },
{ title: "EULA Lullaby", artist: "Fine Print", duration: 120, bpm: 56,
gen: (s,t) => { ["G3","B3","D4","G4","F#4","D4","B3","A3","G3","B3"].forEach((n,i) => s.triggerAttackRelease(n,"2n",t+i*0.7)); },
wave: "sine", color: "#1abc9c" },
{ title: "Wallet on Fire", artist: "DJ Overdraft", duration: 68, bpm: 155,
gen: (s,t) => { ["C3","Eb3","G3","Bb3","C4","Eb4","G4","Eb4","C4","Bb3","G3","Eb3","C3"].forEach((n,i) => s.triggerAttackRelease(n,"16n",t+i*0.15)); },
wave: "sawtooth", color: "#e67e22" },
];
// Mutable song list — replaced by audio/songs.json if available
let SONGS = BUILTIN_SONGS;
const SUBS = {
pause: { name:"Pause Plus™", price:"$2.99/mo", micro:0.01, desc:"The freedom to stop. Whenever you want.", color:"#e74c3c", dismiss:"I like surprises" },
resume: { name:"Resume Rights™", price:"$2.49/mo", micro:0.02, desc:"You paused. Now un-pause. Two different things.", color:"#e67e22", dismiss:"Silence suits me" },
repeat: { name:"Loop Loyalty™", price:"$5.99/mo", micro:0.03, desc:"Hear it again. And again. And again.", color:"#9b59b6", dismiss:"Once was enough" },
unrepeat: { name:"Premium Loop Loyalty™", price:"$8.99/mo", micro:0.05, desc:"Escape the loop. If you dare.", color:"#8e44ad", dismiss:"I'll loop forever" },
skip: { name:"Skip Sprint™", price:"$4.99/mo", micro:0.05, desc:"Life's too short. Songs shouldn't be.", color:"#3498db", dismiss:"I'll wait it out" },
prev: { name:"Nostalgia Pass™", price:"$6.99/mo", micro:0.10, desc:"Go back. Premium memories cost more.", color:"#1abc9c", dismiss:"The past is gone" },
volume: { name:"Volume Freedom™", price:"$3.99/mo", micro:0.08, desc:"Break free from the 23-47% corridor.", color:"#f39c12", dismiss:"35% is fine actually" },
seek: { name:"Precision Seek™", price:"$2.99/mo", micro:0.04, desc:"Go where you mean to go. Radical concept.", color:"#e74c3c", dismiss:"I'll guess" },
shuffle: { name:"Smart Shuffle™", price:"$6.99/mo", micro:0.06, desc:"Actually random. Not weighted against you.", color:"#2ecc71", dismiss:"Order is fine" },
queue: { name:"Queue Capacity Pack™", price:"$1.49/3 slots",micro:0.50, desc:"Your queue is full (1 song max). Expand it.", color:"#e74c3c", dismiss:"One song is plenty" },
dark: { name:"Dark Mode™", price:"$0.99/mo", micro:0.99, desc:"Easy on the eyes. Hard on the wallet.", color:"#34495e", dismiss:"I love bright screens" },
lyrics: { name:"Full Verse™", price:"$3.49/mo", micro:0.07, desc:"See ALL the words. Not just some of them.", color:"#e74c3c", dismiss:"I'll guess the lyrics" },
continue: { name:"Continuity Plan™", price:"$4.99/mo", micro:0.02, desc:"Hear beyond 3 seconds. Revolutionary.", color:"#16a085", dismiss:"3 seconds is enough" },
exit: { name:"Silent Exit™", price:"$1.99/mo", micro:0.25, desc:"Close the app without an unskippable ad.", color:"#c0392b", dismiss:"I love ads actually" },
eq: { name:"EQ Preset Pack™", price:"$0.50/preset",micro:0.50, desc:"Shape your sound. Shape your spending.", color:"#d35400", dismiss:"Flat is a vibe" },
bluetooth: { name:"Wireless Freedom Pass™", price:"$2.99/mo", micro:0.15, desc:"Cut the cord. Not the cost.", color:"#2980b9", dismiss:"Wires are retro" },
like: { name:"Appreciation License™", price:"$1.99/mo", micro:0.25, desc:"Express feelings. For a fee.", color:"#e74c3c", dismiss:"I feel nothing" },
hd: { name:"HD Audio™", price:"$3.99/mo", micro:0.20, desc:"Upgrade from 64kbps Suffering Quality™.", color:"#9b59b6", dismiss:"Lo-fi is a genre" },
history: { name:"Recently Played™", price:"$2.49/mo", micro:0.15, desc:"We know what you listened to. Pay to find out too.", color:"#1abc9c", dismiss:"I'll forget" },
unsub: { name:"Cancellation Processing™",price:"$4.99/unsub", micro:4.99, desc:"Freedom from commitment. Commitment from your wallet.", color:"#c0392b", dismiss:"I'll keep paying" },
};
const LYRICS_DATA = [
[["We","charge","for","every","click"],["Your","wallet","feels","the","pain"],["Subscribe","or","live","in","silence"],["The","music","never","stops"],["Unless","you","pay","to","pause"]],
[["Somewhere","deep","inside","the","EULA"],["A","clause","awaits","your","soul"],["Terms","conditions","fine","print","sorrow"],["Accept","decline","accept","forever"],["Your","data","sings","for","us"]],
[["Micro","transactions","falling","like","rain"],["A","penny","here","a","dollar","there"],["Your","balance","slowly","fading"],["But","music","keeps","on","playing"],["Three","seconds","at","a","time"]],
[["Error","four","oh","four","feeling"],["Not","found","inside","this","app"],["Pay","twenty","five","cents","learn"],["What","went","wrong","this","time"],["Spoiler","it","was","you"]],
[["Hush","little","user","don't","say","word"],["Papa's","gonna","buy","premium","tier"],["And","if","that","premium","runs","out"],["Papa's","gonna","charge","card","again"],["Sleep","tight","wallet","light"]],
[["Fire","fire","wallet","burning","bright"],["Every","button","costs","a","fee"],["The","flames","of","monetization"],["Light","the","path","to","bankruptcy"],["Dance","dance","overdraft","dance"]],
];
const AD_COPY = [
"TIRED OF AFFORDABLE MUSIC? Try Pay2Play! Premium Ultra™ — now with 40% more paywalls!",
"Did you know? The average Pay2Play! user spends $847/year. Be above average.",
"EXCLUSIVE: Unlock the ability to unlock abilities with our Meta-Unlock Bundle™!",
"Pay $19.99/mo to remove this ad. Or $29.99/mo to also remove the next one.",
"Your listening data has been sold to 47 advertisers. Upgrade to Privacy Plus™ ($12.99/mo) to make it 46.",
"Pay2Play! Kids™ — Teach your children the value of money. One paywall at a time.",
"RATE US 5 STARS and receive absolutely nothing in return!",
"Pay2Play! was voted #1 Music Player by people who had no other choice.",
];
const fmt = s => `${Math.floor(s/60)}:${String(Math.floor(s%60)).padStart(2,"0")}`;
const track = (event, props) => { try { window.plausible(event, { props }); } catch(e) {} };
// Cha-ching sound effect via Tone.js
const playChaChing = async () => {
try {
await Tone.start();
const s = new Tone.Synth({
oscillator: { type: "triangle" },
envelope: { attack: 0.001, decay: 0.12, sustain: 0, release: 0.08 },
volume: -10
}).toDestination();
const now = Tone.now();
s.triggerAttackRelease("C7", "32n", now);
s.triggerAttackRelease("E7", "32n", now + 0.08);
setTimeout(() => s.dispose(), 500);
} catch(e) {}
};
// Annoying jingle for the exit ad
const playAdJingle = async () => {
try {
await Tone.start();
const s = new Tone.Synth({
oscillator: { type: "square" },
envelope: { attack: 0.01, decay: 0.1, sustain: 0.3, release: 0.1 },
volume: -15
}).toDestination();
const now = Tone.now();
["C5","E5","G5","C6","G5","E5","C5"].forEach((n, i) =>
s.triggerAttackRelease(n, "16n", now + i * 0.12)
);
setTimeout(() => s.dispose(), 2000);
} catch(e) {}
};
// ============================================================
// COMPONENTS
// ============================================================
function WelcomeBanner({ balance, onStart }) {
const [fading, setFading] = useState(false);
const dismiss = () => { setFading(true); track("click: start_spending"); setTimeout(onStart, 400); };
return (
🎵💸
Pay2Play!
Every feature costs money. Pause, skip, volume — all paywalled.
Your complimentary balance: ${balance.toFixed(2)}
Part of donothireus.com
);
}
function Modal({ sub, phase, successText, onSub, onMicro, onClose, onTopUp }) {
if (!sub) return null;
const s = SUBS[sub];
return (
{phase === "success" ? (
<>
✅
Congratulations!
{successText}
>
) : phase === "broke" ? (
<>
💸
Insufficient Funds
Even your wallet is on a free tier.
>
) : (
<>
⚡ FEATURE LOCKED ⚡
{s.name}
{s.desc}
>
)}
);
}
function ExitAd({ onDone }) {
const [c, setC] = useState(15);
const [adIdx, setAdIdx] = useState(() => Math.floor(Math.random() * AD_COPY.length));
const onDoneR = useRef(onDone);
onDoneR.current = onDone;
useEffect(() => { playAdJingle(); }, []);
useEffect(() => { if(c<=0){onDoneR.current();return;} const t=setTimeout(()=>setC(x=>x-1),1000); return()=>clearTimeout(t); }, [c]);
useEffect(() => { const t=setInterval(()=>setAdIdx(i=>(i+1)%AD_COPY.length),3500); return()=>clearInterval(t); }, []);
return (
UNSKIPPABLE AD
{c}s
{AD_COPY[adIdx]}
Subscribe to Silent Exit™ ($1.99/mo) to skip this
);
}
function CBtn({ onClick, color, size=28, children, label }) {
return (
);
}
// ============================================================
// MAIN APP
// ============================================================
function PayPlay() {
const [ci, setCi] = useState(0);
const [on, setOn] = useState(false);
const [prog, setProg] = useState(0);
const [vol, setVol] = useState(35);
const [rep, setRep] = useState(false);
const [shuf, setShuf] = useState(false);
const [modal, setModal] = useState(null);
const [modalPhase, setModalPhase] = useState("choose");
const [successText, setSuccessText] = useState("");
const [bal, setBal] = useState(() => +(0.50 + Math.random() * 9.50).toFixed(2));
const [subs, setSubs_] = useState({});
const [toastMsg, setToastMsg] = useState("");
const [toastColor, setToastColor] = useState("#00ee44");
const [toastVis, setToastVis] = useState(false);
const [lyr, setLyr] = useState(false);
const [showExit, setShowExit] = useState(false);
const [showAd, setShowAd] = useState(false);
const [spent, setSpent] = useState(0);
const [acts, setActs] = useState(0);
const [toneOk, setToneOk] = useState(false);
const [bars, setBars] = useState(Array(16).fill(2));
const [welcome, setWelcome] = useState(true);
const [bright, setBright] = useState(false);
const [balKey, setBalKey] = useState(0);
const [likes, setLikes] = useState(0);
const [hdAudio, setHdAudio] = useState(false);
const synthR = useRef(null);
const loopR = useRef(null);
const progT = useRef(null);
const visT = useRef(null);
const hitLimit = useRef(false);
const audioR = useRef(null);
const filterR = useRef(null);
const milestoneR = useRef(new Set());
const song = SONGS[ci];
const has = k => subs[k];
// Theme-aware colors (toggle when Dark Mode joke triggers bright mode)
const bg = bright ? "#f5f5f0" : "#0d0d1a";
const card = bright ? "#e8e8f0" : "#1c1c32";
const text = bright ? "#111" : "#fff";
const muted = bright ? "#555" : "#7788aa";
const accent= bright ? "#00aa33" : "#00ee44";
// Sync body background with theme
useEffect(() => { document.body.style.background = bg; }, [bright]);
// Spending milestones
useEffect(() => {
const checks = [[1,"Over $1 spent. Getting started."],[5,"$5 down. Commitment issues?"],[10,"$10 gone. You could buy actual music."],[20,"$20. Seek professional help."]];
checks.forEach(([threshold, msg]) => {
if (spent >= threshold && !milestoneR.current.has(threshold)) {
milestoneR.current.add(threshold);
setTimeout(() => flash(msg, "#f39c12"), 600);
}
});
}, [spent]);
// Color-coded toasts: red for charges, green for unlocks, yellow for info
const flash = useCallback((msg, color = "#00ee44") => {
setToastMsg(msg); setToastColor(color); setToastVis(true);
setTimeout(() => setToastVis(false), 2500);
}, []);
// Charge with cha-ching sound + balance flash animation
const charge = useCallback((a, l) => {
setBal(b => Math.max(0, +(b - a).toFixed(2)));
setSpent(t => +(t + a).toFixed(2));
setActs(x => x + 1);
setBalKey(k => k + 1);
flash(`-$${a.toFixed(2)} • ${l}`, "#e74c3c");
playChaChing();
}, [flash]);
const tryAct = (k, fn) => {
track("click: " + k, { subscribed: !!has(k) });
if (has(k)) { fn(); return; }
setModal({ key: k, action: fn });
setModalPhase("choose");
};
// Modal: subscribe (full price)
const onSubClick = () => {
if (!modal) return;
const s = SUBS[modal.key], p = parseFloat(s.price.replace(/[^0-9.]/g, ""));
if (bal < p) { setModalPhase("broke"); return; }
charge(p, s.name);
setSubs_(prev => ({ ...prev, [modal.key]: true }));
track("subscribe", { feature: modal.key, price: p });
setSuccessText(`${s.name} is yours. Was it worth it? (It wasn't.)`);
setModalPhase("success");
setTimeout(() => { modal.action(); setModal(null); setModalPhase("choose"); }, 1800);
// Humor: pause and resume are separate products
if (modal.key === "pause") setTimeout(() => flash("Note: unpausing requires Resume Rights™", "#f39c12"), 2500);
};
// Modal: one-time micro-transaction
const onMicroClick = () => {
if (!modal) return;
const s = SUBS[modal.key];
if (bal < s.micro) { setModalPhase("broke"); return; }
charge(s.micro, `${s.name} (1x)`);
track("microtransaction", { feature: modal.key, price: s.micro });
setSuccessText(`One-time access. That's $${s.micro.toFixed(2)} you'll never see again.`);
setModalPhase("success");
setTimeout(() => { modal.action(); setModal(null); setModalPhase("choose"); }, 1500);
};
// Modal: top-up from insufficient-funds screen (with fee)
const handleModalTopUp = () => {
setBal(b => +(b + 9.50).toFixed(2));
setSpent(s => +(s + 0.50).toFixed(2));
setActs(x => x + 1);
setBalKey(k => k + 1);
flash("+$9.50 ($0.50 processing fee)", "#f39c12");
playChaChing();
track("topup", { source: "modal" });
setModalPhase("choose");
};
// ---- Audio engine ----
const initTone = useCallback(async () => { if (!toneOk) { await Tone.start(); setToneOk(true); } }, [toneOk]);
const stopAudio = useCallback(() => {
if (audioR.current) { audioR.current.pause(); audioR.current.currentTime = 0; audioR.current = null; }
if (loopR.current) { loopR.current.stop(); loopR.current.dispose(); loopR.current = null; }
if (synthR.current) { synthR.current.dispose(); synthR.current = null; }
if (filterR.current) { filterR.current.dispose(); filterR.current = null; }
try { Tone.getTransport().stop(); } catch(e) {}
clearInterval(progT.current); clearInterval(visT.current);
setBars(Array(16).fill(2));
}, []);
const startAudio = useCallback(async (idx, from = 0) => {
stopAudio(); hitLimit.current = false;
const s = SONGS[idx];
// HTML5 Audio for self-hosted MP3s
if (s.url) {
const audio = new Audio(s.url);
audio.currentTime = from;
audio.volume = vol / 100;
audioR.current = audio;
audio.play().catch(() => {});
let p = from;
progT.current = setInterval(() => {
p = Math.floor(audio.currentTime);
setProg(p);
if (p >= s.duration || audio.ended) { clearInterval(progT.current); stopAudio(); setOn(false); setProg(0); }
}, 1000);
visT.current = setInterval(() => setBars(b => b.map(() => 2 + Math.random() * 28)), 150);
return;
}
// Tone.js procedural synth (low-pass filtered unless HD Audio purchased)
await initTone();
const filter = new Tone.Filter(hdAudio ? 20000 : 1200, "lowpass").toDestination();
filterR.current = filter;
const synth = new Tone.Synth({ oscillator: { type: s.wave }, envelope: { attack: 0.05, decay: 0.2, sustain: 0.4, release: 0.8 }, volume: -30 + vol * 0.3 }).connect(filter);
synthR.current = synth;
const loop = new Tone.Loop(time => s.gen(synth, time), 2.5);
loopR.current = loop;
Tone.getTransport().bpm.value = s.bpm;
Tone.getTransport().start(); loop.start(0);
let p = from;
progT.current = setInterval(() => { p += 1; setProg(p); if (p >= s.duration) { clearInterval(progT.current); stopAudio(); setOn(false); setProg(0); } }, 1000);
visT.current = setInterval(() => setBars(b => b.map(() => 2 + Math.random() * 28)), 150);
}, [initTone, stopAudio, vol, hdAudio]);
useEffect(() => () => stopAudio(), [stopAudio]);
// 3-second free limit with audio degradation at 2s
useEffect(() => {
if (!on || has("continue")) return;
// Muffle audio 1 second before cutoff
if (prog === 2) {
if (synthR.current) synthR.current.volume.rampTo(-40, 0.8);
if (audioR.current) audioR.current.volume = 0.1;
}
// Hard cutoff at 3 seconds
if (prog >= 3 && !hitLimit.current) {
hitLimit.current = true; stopAudio(); setOn(false);
setModal({ key: "continue", action: () => { hitLimit.current = false; setOn(true); startAudio(ci, prog); } });
setModalPhase("choose");
}
}, [prog, on, subs]);
// ---- User actions ----
const handlePlay = async () => {
track(on ? "click: pause" : "click: play", { song: song.title });
if (on) { tryAct("pause", () => { stopAudio(); setOn(false); }); }
else {
if (prog > 0 && !has("continue")) {
tryAct("resume", () => { setOn(true); startAudio(ci, prog); });
} else {
if (!SONGS[ci].url) await initTone();
hitLimit.current = false; setOn(true); startAudio(ci, prog);
}
}
};
const changeSong = idx => { stopAudio(); setCi(idx); setProg(0); setOn(false); hitLimit.current = false; setBars(Array(16).fill(2)); };
const handleSeek = e => {
const r = e.currentTarget.getBoundingClientRect(), pct = (e.clientX - r.left) / r.width, tgt = Math.floor(pct * song.duration);
track("click: seek", { subscribed: !!has("seek") });
if (has("seek")) { setProg(tgt); if (on) { stopAudio(); startAudio(ci, tgt); } }
else { setProg(Math.max(0, Math.min(song.duration, tgt + Math.floor(Math.random() * 40) - 15))); flash("Seek landed somewhere in the general vicinity.", "#f39c12"); }
};
const handleSeekKey = e => {
let tgt = prog;
if (e.key === "ArrowRight") tgt = Math.min(song.duration, prog + 5);
else if (e.key === "ArrowLeft") tgt = Math.max(0, prog - 5);
else return;
e.preventDefault();
if (has("seek")) { setProg(tgt); if (on) { stopAudio(); startAudio(ci, tgt); } }
else { setProg(Math.max(0, Math.min(song.duration, tgt + Math.floor(Math.random() * 40) - 15))); flash("Seek precision is approximate. Very approximate.", "#f39c12"); }
};
const handleVol = e => {
const v = parseInt(e.target.value);
if (!has("volume") && (v < 23 || v > 47)) { tryAct("volume", () => { setVol(v); if (synthR.current) synthR.current.volume.value = -30 + v * 0.3; if (audioR.current) audioR.current.volume = v / 100; }); return; }
setVol(v); if (synthR.current) synthR.current.volume.value = -30 + v * 0.3; if (audioR.current) audioR.current.volume = v / 100;
};
const shareReceipt = () => {
const lines = [
"Pay2Play! Receipt", "",
`Total spent: $${spent.toFixed(2)}`,
`Actions: ${acts} (${acts > 0 ? `$${(spent/acts).toFixed(2)}/action` : "free"})`,
`Subscriptions: ${Object.keys(subs).length}`,
Object.keys(subs).length > 0 ? Object.keys(subs).map(k => SUBS[k].name).join(", ") : "None (cheapskate)",
"", `Try it: ${window.location.href}`
];
if (navigator.clipboard) {
navigator.clipboard.writeText(lines.join("\n")).then(() => { flash("Receipt copied! Share your shame.", accent); track("share_receipt", { spent: spent.toFixed(2) }); });
}
};
// Volume slider gradient: shows free zone (23-47%) when not subscribed
const volBg = has("volume")
? `linear-gradient(to right,${song.color} ${vol}%,${card} ${vol}%)`
: `linear-gradient(to right,${card} 0%,${card} 23%,${song.color} 23%,${song.color} ${vol}%,${song.color}33 ${vol}%,${song.color}33 47%,${card} 47%)`;
const curLyrics = LYRICS_DATA[ci] || LYRICS_DATA[0];
const toastTextColor = ["#e74c3c","#f39c12"].includes(toastColor) ? "#fff" : "#000";
// ============================================================
// RENDER
// ============================================================
return (
<>
{welcome && setWelcome(false)} />}
{showAd && { setShowAd(false); setShowExit(true); }} />}
{/* Exit / receipt screen */}
{showExit && (
👋
Thanks for listening!
Total spent: ${spent.toFixed(2)}
{acts} actions • ${acts>0?(spent/acts).toFixed(2):"0.00"}/action
{Object.keys(subs).length} subscription{Object.keys(subs).length !== 1 ? "s" : ""}
)}
{ setModal(null); setModalPhase("choose"); }} onTopUp={handleModalTopUp} />
{/* Toast notification */}
{toastMsg}
{/* ---- Main player ---- */}
{/* Sticky header with balance */}
{/* Active subscriptions marquee (speed scales with count) */}
{Object.keys(subs).length > 0 && (
{Object.keys(subs).map(k => ✓ {SUBS[k].name})}
)}
{/* Visualizer (responsive width) */}
{bars.map((h, i) =>
)}
{/* Vinyl disc — freezes in place on pause */}
FREE TIER
{/* Free-listening countdown badge */}
{on && !has("continue") && prog < 3 && (
FREE {3 - prog}s
)}
{!toneOk && !SONGS[0].url &&
Click play to start audio
}
{/* Song info */}
{song.title}
{song.artist}
{/* Seek bar — larger touch target, keyboard accessible */}
{fmt(prog)}
{!has("seek") && 🔒 approx.}
{fmt(song.duration)}
{/* Transport controls */}
tryAct("shuffle", () => setShuf(s => !s))} color={shuf ? accent : muted} label="Shuffle">🔀
tryAct("prev", () => changeSong((ci - 1 + SONGS.length) % SONGS.length))} color={muted} size={36} label="Previous">⏮
tryAct("skip", () => changeSong((ci + 1) % SONGS.length))} color={muted} size={36} label="Next">⏭
rep ? tryAct("unrepeat", () => setRep(false)) : tryAct("repeat", () => setRep(true))} color={rep ? accent : muted} label="Repeat">🔁
{/* Volume slider with free-zone indicator */}
🔈
{vol}%
{!has("volume") && 🔒 23-47%}
{!hdAudio && !song.url &&
🔊 64kbps Suffering Quality™ — Upgrade to HD Audio
}
{/* Feature buttons (responsive grid) */}
{[
{ l:"Lyrics", i:"📝", a:() => tryAct("lyrics", () => setLyr(x => !x)) },
{ l:"HD Audio", i:"🔊", a:() => tryAct("hd", () => { setHdAudio(true); if(filterR.current) filterR.current.frequency.rampTo(20000, 0.5); flash("HD Audio enabled. Hear the difference? (There isn't one.)", "#9b59b6"); }) },
{ l:"Like", i:"❤️", a:() => tryAct("like", () => { setLikes(l => l + 1); flash(`Liked! (${likes + 1} total). This does absolutely nothing.`, "#e74c3c"); }) },
{ l:"History", i:"📜", a:() => tryAct("history", () => flash(`Recently played: "${song.title}". That's it. You're still on it.`, "#1abc9c")) },
{ l:"EQ", i:"🎛️", a:() => tryAct("eq", () => flash("EQ: Flat ($0.50). Bass Boost: another $0.50. Each.", "#f39c12")) },
{ l:"Bluetooth", i:"📡", a:() => tryAct("bluetooth", () => flash("Bluetooth connected... for now.", "#3498db")) },
{ l:"Dark Mode", i:"🌙", a:() => tryAct("dark", () => { setBright(b => !b); flash(!bright ? "Welcome to Light Mode™. Suffering." : "Dark Mode restored. You paid for the default.", !bright ? "#f39c12" : "#00ee44"); }) },
{ l:"Queue +", i:"📋", a:() => tryAct("queue", () => flash("Queue expanded to 4 slots! (3 are decorative)", "#00ee44")) },
].map(f => )}
{/* Lyrics panel — animated slide */}
LYRICS {!has("lyrics") && "— REDACTED VERSION"}
{curLyrics.map((line, i) => (
{has("lyrics") ? line.join(" ") : line.map((w, j) => {j % 3 === 0 ? w : █████}{" "})}
))}
{/* Up Next — card styled, with price on lock icons */}
UP NEXT
{SONGS.map((s, i) => i !== ci && (
tryAct("skip", () => changeSong(i))} style={{display:"flex",alignItems:"center",gap:12,padding:"10px 20px",cursor:"pointer",borderBottom:`1px solid ${bright?"#ddd":muted+"11"}`}}>
{fmt(s.duration)}
🔒 ${SUBS.skip.micro.toFixed(2)}
))}
{/* Session Economics */}
SESSION ECONOMICS
{[["Spent",`$${spent.toFixed(2)}`,"#e74c3c"],["Subscriptions",Object.keys(subs).length,"#f39c12"],["Actions",acts,text],["Cost/action",acts>0?`$${(spent/acts).toFixed(2)}`:"—","#e74c3c"]].map(([l,v,c]) => (
{l}{v}
))}
>
);
}
// Load real songs from audio/songs.json if available, then render
fetch("audio/songs.json")
.then(r => { if (r.ok) return r.json(); throw new Error(); })
.then(songs => { if (Array.isArray(songs) && songs.length > 0) SONGS = songs; })
.catch(() => {}) // No songs.json — use procedural builtins
.finally(() => ReactDOM.createRoot(document.getElementById("root")).render());