A visible countdown turns a ten-second challenge into a reaction test. I wanted the other version: start the clock, hide the elapsed value, and stop when your internal clock says the target has arrived. The implementation is tiny, but using the right browser clock and keeping the target hidden are what make the game meaningful. I wrote this as a small example for someone building a perception game where the measurement should describe the button press, not the user's ability to read a timer. performance.now() measures the round On start, the component chooses the target, clears the old result, records a monotonic start time, and marks the round as running: function startChallenge() { if (!hasBrowser()) return; pickTarget(); lastResult.value = null; startAt.value = performance.now(); isRunning.value = true; } function stopChallenge() { if (!hasBrowser() || !isRunning.value) return; const elapsedMs = performance.now() - startAt.value; const targetMs = activeTargetSec.value * 1000; const errorMs = elapsedMs - targetMs; const errorPct = (errorMs / targetMs) * 100; // store elapsedMs, errorMs, and errorPct in the result } performance.now() is the important choice. It is intended for elapsed measurements and is not based on the calendar clock that can jump when the operating system synchronizes time. The result keeps milliseconds, then derives signed error and error percentage from the target duration. If a player stops at 9.842 seconds for a 10-second target, the error is -158 milliseconds and -1.58 percent; stopping at 10.3 seconds makes both values positive. There is no interval ticking in the round itself. The page does not need to display “9, 8, 7” and accidentally teach the answer. The browser records a start reading and an end reading around the user's two clicks. The click event still has normal input latency, so this is a game measurement, not laboratory equipment. Random targets are selected at start time The fixed modes are 5, 10, and 30 seconds. Random mode samples from [5, 7, 10, 12, 15, 20, 30] inside pickTarget(), after Start is pressed: const RANDOM_TARGETS = [5, 7, 10, 12, 15, 20, 30]; function pickTarget() { if (targetMode.value === "random") { activeTargetSec.value = RANDOM_TARGETS[Math.floor(Math.random() * RANDOM_TARGETS.length)]; return; } activeTargetSec.value = Number(targetMode.value) || 10; } Selecting random at start prevents the player from memorizing a value while setting up. The active target can be shown in the pre-round UI for fixed modes, but the elapsed value disappears once the round is running. A 5-second attempt feels very different from a 30-second attempt: short rounds magnify click timing, while long rounds invite internal counting and drift. That is why comparisons are most useful within one mode. The target is stored in each result along with the elapsed milliseconds, so a history row can honestly say whether an attempt was for 7 or 20 seconds. It would be a mistake to compare raw errors across targets without considering their scale: 200 milliseconds is 4% of five seconds but only about 0.67% of 30 seconds. The source keeps both errorMs and errorPct, allowing the interface to show the absolute miss while the rating logic uses the relative one. The error bar is only a visualization: const pct = Math.min(100, Math.abs(lastResult.value.errorPct) * 4); return `${pct}%`; It scales absolute error for a readable bar, caps at 100%, and does not alter the stored error or score. A large miss can therefore saturate the graphic without pretending that a 40% miss is equivalent to a 5% miss. The rating thresholds are also separate from the bar. getRatingKey() uses absolute error in milliseconds: up to 50 ms is the top label, followed by 150, 400, and 1,000 ms boundaries. That makes the feedback easy to explain, while the percentage remains useful for comparing targets of different lengths. A 200 ms miss is 4% of five seconds but only about 0.67% of 30 seconds. History is local and deliberately bounded After a round, the component prepends the result to an array saved under begoodtool_tenSecondChallenge_v1. saveRecord keeps only the first 50 records. The screen derives recentRecords by taking the ten most recent, while bestRecord sorts all retained records by absolute error: function saveRecord(record) { const records = [record, ...loadRecords()] .slice(0, HISTORY_LIMIT); saveRecords(records); historyVersion.value++; } const recentRecords = computed(() => allRecords.value.slice(0, RECENT_LIMIT)); This keeps the page useful for practice without creating an account or sending timing data anywhere. Clearing history removes the local records and the current result. A private browsing session, cleared site data, or a different browser profile can remove the history, which is a privacy property and an inconvenience at the same time. For a repeatable personal drill, I would keep the mode fixed, do ten attempts, and compare the distribution rather than celebrating one lucky click. Random mode is better for a party because nobody can settle into a memorized rhythm. Neither mode changes the underlying measurement: only the interval between the two button events is recorded. The limitation is the point: focus, fatigue, music, counting strategy, display refresh, and the precision of a button press all affect the result. Randomness is not seeded, so repeated random-mode rounds are not reproducible experiments. I turned the experiment into a small free tool: 10-Second Intuition Timer Challenge.