A menu should move once when you press Down, wait briefly, then scroll while you hold it. Reading the held level on every update is too fast; reading only new presses never repeats. This routine supplies the time policy between those choices.

Include and initialise
Reserve H/W/T/P/D/R/O and lines 3000–3210. There are no arrays or private scratch globals. Combine routines.bas and your caller with build.py, preventing fall-through with END, STOP or a jump. C64/BBC use GOSUB; Spectrum uses GO SUB.
Call 3000 when the menu gains control or another intentional reset occurs. It clears every public field, including configuration. Set positive delay D and repeat interval R afterwards, each one through 255 service calls. Use separate renamed fields/line references for independently repeating controls.
Feed a held level every update
H is a held byte: zero means release, any nonzero byte means held. W is retained normalised history, zero or one; T is the retained byte timer. Call 3100 regularly, then consume fresh pulse P. O=1 means the state and configuration were valid.
Reusable numbered routines
3000 LET H=0: LET W=0: LET T=0: LET P=0: LET D=0: LET R=0: LET O=0: RETURN
3100 LET P=0: LET O=0
3110 IF H<0 OR H>255 OR H<>INT (H) THEN RETURN
3120 IF W<>0 AND W<>1 THEN RETURN
3130 IF T<0 OR T>255 OR T<>INT (T) THEN RETURN
3140 IF D<1 OR D>255 OR D<>INT (D) THEN RETURN
3150 IF R<1 OR R>255 OR R<>INT (R) THEN RETURN
3160 LET O=1
3170 IF H=0 THEN LET W=0: LET T=0: RETURN
3180 IF W=0 THEN LET W=1: LET T=D: LET P=1: RETURN
3190 IF T>0 THEN LET T=T-1
3200 IF T=0 THEN LET P=1: LET T=R
3210 RETURNA released call clears W/T and returns P=0. The first held call sets W=1, returns P=1 immediately and loads T=D. Later held calls decrement positive T; reaching zero produces P=1 and reloads T=R. A retained zero timer on an already held control fires immediately and reloads R.
| Fields | Use |
|---|---|
| H | Supplied held level |
| W / T | Retained held history / countdown |
| D / R | First delay / later repeat interval |
| P / O | Fresh repeat pulse / valid-input flag |
H/T must be integral bytes, W exactly zero or one, and D/R positive integral bytes. Invalid state clears P/O and preserves the history, timer and supplied inputs. The helper normalises W during valid service; it does not turn a raw keyboard scan into H.
Watch the pulse sequence
For eight held calls, D=4,R=2 produces pulses on indices zero, four and six. The custom D=6,R=3 produces zero and six. The saved checks verify pulse count and final timer. The later release/repress confirms that a fresh hold gets an immediate pulse again.
Example calls from the maintained caller
70 GO SUB 3000: LET AA=0: LET BB=0
80 LET H=1: LET D=4: LET R=2: LET NN=0: FOR Q=1 TO 8
90 GO SUB 3100: GO SUB 2000: LET NN=NN+P: NEXT Q
100 IF NN=3 THEN LET AA=1
110 IF T=1 THEN LET BB=1
120 LET H=0: GO SUB 3100: GO SUB 2000
130 LET H=1: GO SUB 3100: GO SUB 2000
140 PRINT "REPT RESULTS";AA;BB
150 POKE MB+995,AA: POKE MB+996,BB: POKE MB+997,GD: POKE MB+998,WC-256*INT (WC/256): POKE MB+999,INT (WC/256): POKE MB+1000,239: STOPLine 2000, AA/BB/NN/JJ/KK (Z/Q on Spectrum) and the mailbox are diagnostic machinery. In a menu, move selection on P and use a separately validated navigation routine for bounds or wrapping. Resolve opposing directions before repeating, or give their repeat contexts an explicit arbitration policy.
Customise the feel
A delay of one repeats on the following held service; an interval of one repeats every later service. These are update counts, not seconds. Slow or irregular service changes the physical repeat rate. An elapsed-time version can use deadlines when the game already owns a suitable clock.
Focus changes need a rule. Resetting on every menu frame defeats the delay; never resetting can carry stale history into a different context. Changing D/R affects later reloads, not the already running T. Acceleration after a long hold needs another counter and its own delay policy.
Mistake and repair
The wrong caller clears W/T before every held sample. Each update looks like the first press, so all eight samples pulse. Both saved count and timer checks fail. Retain history between services and reset at intentional focus/session boundaries. Repaired source and loaded program match normal exactly.

Cost and Maths for Games
Each call uses fixed validation and one small state transition, with no input scan, busy wait or allocation. A pure press-edge detector is smaller when repetition is unwanted. Hardware or interpreter key-repeat behaviour may not match a controller or a game’s chosen cadence.
Maths for Games connects state transitions and discrete-time boundaries. The Craft connects delay, cadence and menu responsiveness to how the player experiences a held control.
Global names and line numbers
BASIC numeric names, arrays and numbered lines are global. To combine helpers, rename public and private fields and renumber the helper together with every GO SUB/GO TO reference. Keep each state block under one owner. The maintained build.py rejects duplicate line numbers; it does not automatically namespace or renumber routines.
Machine and measured size
Spectrum FOR variables must be single letters: this diagnostic uses Z/Q instead of the other callers’ JJ/KK. This helper uses scalar numeric fields and allocates no arrays. AND/OR in these listings join comparisons; neither is being used as a bitwise operator. The diagnostic reserves its mailbox above CLEAR 59999.
The numbered helper occupies 443 tokenised program bytes on this interpreter, including line headers. This excludes the caller, test tables, numeric scalar/array storage, interpreter stacks and ROM. Tokenised size is not machine-code size or a hardware-cycle count. No real-time or whole-game performance budget is inferred from the diagnostic. The caller’s AA/BB/NN, Z/Q and RAM mailbox are observation machinery, not required game state.
Execution evidence
Working, custom, deliberately wrong and exact repaired callers executed through Spectrum’s stock BASIC interpreter under Emu198x 0.44.2. Build198x 0.2.9 tokenises C64 PRG and Spectrum TAP programs. C64 enters stock RUN, Spectrum uses ROM tape loading, and BBC enters every numbered source line through MOS/BASIC. Actual loaded program bytes, complete state packets, positive completion/count/guards and visible result pixels are checked. After the positive completion marker, the observer advances two complete display frames before capturing, so a partially scanned final character cannot masquerade as a finished picture. Firmware is not distributed.
An independent mathematical/order model checks the trace and rejects altered source, configuration, loaded program, state, completion and image evidence. Host exhaustive checks complement the native boundary/sequential examples; those host ranges are not claimed as natively exhaustive. The supplied diagnostic callers deliberately end with STOP; the interpreter report is expected. The wrong caller prints 0,0 and the normal/custom/repaired callers print 1,1.
Public CI checks retained evidence; it does not execute restricted firmware. Refresh with python3 scripts/verify-basic-temporal.py input-repeat sinclair-zx-spectrum --tools /path/to/extracted/releases and inspect with python3 scripts/check-basic-temporal.py input-repeat.
Explore further
Compare the assembly technique. The BASIC game-flow routine makes session boundaries explicit. Complete source and native evidence.