/* global React, ReactDOM */ const { useState, useEffect, useRef } = React; const TWEAKS_DEFAULTS = /*EDITMODE-BEGIN*/{ "paletteWarmth": "noon", "heroDensity": "full", "reducedMotionPreview": false, "showScrollCue": true }/*EDITMODE-END*/; function App() { const [tweaks, setTweak] = window.useTweaks ? window.useTweaks(TWEAKS_DEFAULTS) : [TWEAKS_DEFAULTS, () => {}]; const [submitted, setSubmitted] = useState(false); const [submittedFooter, setSubmittedFooter] = useState(false); const [submitting, setSubmitting] = useState(null); // 'hero' | 'footer' | null const [submitError, setSubmitError] = useState(null); const [hammerTwitch, setHammerTwitch] = useState(false); const [scrollSpeed, setScrollSpeed] = useState(0); const [scrollY, setScrollY] = useState(0); const lastScrollY = useRef(0); const lastScrollT = useRef(performance.now()); const decayRef = useRef(0); // Detect reduced motion (system or tweak) const systemReduced = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; const reduced = systemReduced || tweaks.reducedMotionPreview; // Hammer twitch every 3-4s useEffect(() => { if (reduced) return; let t; const loop = () => { const delay = 3000 + Math.random() * 1500; t = setTimeout(() => { setHammerTwitch(true); setTimeout(() => setHammerTwitch(false), 220); loop(); }, delay); }; loop(); return () => clearTimeout(t); }, [reduced]); // Scroll handler — parallax + dust speed useEffect(() => { const onScroll = () => { const y = window.scrollY; const now = performance.now(); const dt = Math.max(1, now - lastScrollT.current); const dy = y - lastScrollY.current; const v = Math.min(1, Math.abs(dy) / dt * 12); decayRef.current = Math.max(decayRef.current, v); lastScrollY.current = y; lastScrollT.current = now; setScrollY(y); }; window.addEventListener("scroll", onScroll, { passive: true }); let raf; const decay = () => { decayRef.current *= 0.92; setScrollSpeed(decayRef.current); raf = requestAnimationFrame(decay); }; decay(); return () => { window.removeEventListener("scroll", onScroll); cancelAnimationFrame(raf); }; }, []); // Apply reduced-motion class to body useEffect(() => { document.body.classList.toggle("reduced-motion", reduced); }, [reduced]); const handleSubmit = async (e, which) => { e.preventDefault(); if (submitting) return; // prevent double-submit const form = e.currentTarget; const email = form.email.value.trim(); if (!email) return; setSubmitting(which); setSubmitError(null); try { const data = new FormData(); data.append("email", email); data.append("metadata__source", which); // 'hero' or 'footer' data.append("embed", "1"); await fetch( "https://buttondown.com/api/emails/embed-subscribe/dueling", { method: "POST", body: data, mode: "no-cors", } ); // 'no-cors' means we can't read the response. Buttondown sends a // double-opt-in confirmation email; the user clicks the link to confirm. if (which === "hero") setSubmitted(true); else setSubmittedFooter(true); } catch (err) { setSubmitError( "We couldn't send your telegraph. Check your connection and try again." ); } finally { setSubmitting(null); } }; const heroParallax = Math.min(scrollY * 0.15, 200); const buildingsParallax = Math.min(scrollY * 0.08, 100); return ( <> {/* ================== HEADER / HERO ================== */}

Dueling

{!submitted ? (
handleSubmit(e, "hero")} action="https://buttondown.com/api/emails/embed-subscribe/dueling" method="post" aria-label="Sign up to be notified" > {submitError && submitting !== "hero" && (

{submitError}

)}
) : (
— Western Union —

Telegraph received. We'll send word.

)} {!submitted && (

No spam. One message when the doors open.

)}
{tweaks.showScrollCue && ( )}
{/* ================== MAIN ================== */}
About

Two duelists. One street. Your phone is your draw, your aim, your shot. Dueling is a mobile motion-sensor reflex shooter that asks the question every Western asks: who's faster. We're building it now.

{/* PILLARS */}
I.

Reflex through motion

II.

Fair by server, always

III.

Free-to-play, respectfully

{/* ROSTER */}
The Roster

The duelists you'll face.

Riley
Doc
Cassidy
Black-Eye
{/* REPEATED FORM */}

When the doors open, we'll wire you first.

One telegram. No social. No spam.

{!submittedFooter ? (
handleSubmit(e, "footer")} action="https://buttondown.com/api/emails/embed-subscribe/dueling" method="post" aria-label="Sign up to be notified (repeated)" > {submitError && submitting !== "footer" && (

{submitError}

)}
) : (
— Western Union —

Telegraph received. We'll send word.

)}
{/* ================== FOOTER ================== */} {/* ================== TWEAKS PANEL ================== */} {/* Hidden in production. Visit ?tweaks to enable. */} {window.TweaksPanel && typeof window !== "undefined" && new URLSearchParams(window.location.search).has("tweaks") && ( setTweak("heroDensity", v)} options={[ { value: "sparse", label: "Sparse" }, { value: "full", label: "Full" }, ]} /> setTweak("reducedMotionPreview", v)} /> setTweak("showScrollCue", v)} /> )} ); } ReactDOM.createRoot(document.getElementById("root")).render();