9.1 KiB
Timer Focus Mode and Hold-Cancel Work Report
#Overview
Today the standalone timer screen was reworked further with a focus on the active sprint layout, countdown ownership, and the hold-to-cancel interaction.
The main direction was to make the running timer feel more like a focused study state instead of a duration picker that happens to count down. The countdown was moved toward a separate overlay, the task details were given more visual emphasis, and the cancel interaction was changed from a simple button press into a deliberate hold action.
#ImplementedFeatures
#CountdownOverlay
Moved the active countdown away from the duration picker:
- Removed the old selected-picker countdown state
- Added a separate countdown overlay using
countdownAnimation - Added
focusModeAnimationso the countdown can move from the central timer area toward the upper-left area - Kept the picker responsible for duration values only
This separates two responsibilities that had previously been mixed together: the picker selects a duration, while the overlay shows active countdown time.
#FocusModeLayout
Adjusted the active timer layout to put more attention on the task:
- Moved task details higher and closer to the center of the running screen
- Increased the task title and description size
- Kept task details animated through
taskDetailsAnimation - Continued using the red screen overlay as the main visual timer-progress element
The intent is for the active state to feel more like a study-session spotlight, where the selected task becomes the main focus and the countdown becomes supporting information.
#HoldToCancel
Changed the cancel action into a hold interaction:
- Added
HOLD_TO_CANCEL_MS - Added
cancelHoldTimeoutRef - Added a hold-completion haptic warning
- Kept the cancel button scale feedback during press
- Changed the label to
Hold to end sprint
This makes cancellation more deliberate and reduces the chance of accidentally ending a sprint with a single tap.
#CancelAccelerationExperiment
Implemented the red timer overlay as cancel feedback:
- Added delayed cancel acceleration through
CANCEL_ANIMATION_DELAY_MS - Added
cancelHoldAnimationDelayRef - Added
cancelAccelStartedRefto distinguish quick taps from actual hold acceleration - Split normal timer progress into
progressAnimationRef - Added
startProgressAnimation(fromY)so progress can start or resume from a specific overlay position - Added
cancelOverlayAnimationas a temporary visual offset on top of the real timer progress - Added
getCancelOverlayTarget(...)to calculate how far the cancel preview should move - Added a release handoff animation so the cancel offset eases back into the real timer position
- Added clamping so the visual overlay does not move past the finished timer position
- Added easing constants for the cancel delay, release handoff, and timer reset timings
The goal was for the red overlay to speed toward the finished position during a hold, then return smoothly to the real timer progress if the user releases before the cancel completes. The important change is that cancel preview motion is now layered on top of the real progress instead of directly taking over the main timer animation.
#DurationPickerCleanup
Cleaned up the duration picker after moving countdown ownership out of it:
- Removed selected countdown rendering from the picker item
- Kept picker items rendering plain timer values
- Kept picker values fading out during active timer mode
- Added index clamping when reading the selected duration from
onMomentumScrollEnd - Restored
durationas a dependency of the start callback so the selected picker value is used correctly
This fixed the earlier issue where the timer could behave as if the selected duration was still the initial value.
#TimerCodeCleanup
Cleaned up the timer screen structure after the interaction behavior was stabilized:
- Renamed the old
animationcallback tostartTimer - Renamed unclear animated values like
opacityandtranslateYtostartButtonOpacityandstartButtonTranslateY - Grouped refs by purpose: animated values, timer/session refs, and cancel-hold refs
- Extracted
clearCountdown,clearCancelHoldTimers, andstopTimerAnimations - Extracted the cancel overlay target calculation into
getCancelOverlayTarget(...) - Split the render section into local render helpers for the overlay, start button, cancel button, countdown, duration picker, and task details
- Moved the timer item layout into
styles.timerItem
This did not change the screen into a separate hook or split the timer into multiple files. The cleanup stayed local to timer.tsx so the current animation work remains easy to inspect.
#LearningNotes
#AnimationOwnership
The main lesson today was that an Animated.Value should have one clear owner at a time.
The red overlay now combines two animated values:
- normal timer progress
- hold-to-cancel visual offset
The normal timer progress is controlled by timerAnimation, while cancel preview motion is controlled by cancelOverlayAnimation. This avoids stopping the real timer progress just to show the cancel speed-up effect.
#RefsAsMutableState
Several refs were added to track animation and timer ownership:
progressAnimationReftracks the long-running red overlay progress animationsessionStartedAtReftracks the progress timeline used for recovery calculationssessionDurationMsRefstores the current timer duration in millisecondscancelHoldTimeoutReftracks when hold cancellation should completecancelHoldAnimationDelayReftracks when cancel acceleration should begincancelAccelStartedReftracks whether the red overlay acceleration actually startedcancelHoldActiveRefandcancelHoldIdRefprevent stale delayed hold callbacks from taking over after release
The important distinction is that assigning to .current is allowed even when the ref variable itself is declared with const.
#CancelOffsetHandoff
The release recovery logic was changed to avoid rewriting the real timer progress:
- keep
timerAnimationrunning as the source of real timer progress - add
cancelOverlayAnimationon top of it while the cancel button is held - animate only the cancel offset back to
0when the hold is released - keep the visible overlay clamped to the screen height
- tune the release handoff timing with
CANCEL_RELEASE_MS
This makes the visual red overlay return to the countdown's real timer position without forcing the main timer animation to stop and restart.
#CurrentState
The hold-cancel red overlay interaction has been reworked so the cancel preview no longer directly mutates the real timer progress.
The current implementation:
- keeps the countdown and real timer progress owned by
timerAnimation - uses
cancelOverlayAnimationas a temporary visual offset during hold-to-cancel - invalidates stale hold callbacks with
cancelHoldIdRef - eases the cancel offset back to
0on release - keeps the cancel-completion path separate from normal timer completion
This should make the red overlay speed-up feel connected to the cancel hold while still keeping the timer progress visually aligned with the countdown after release.
#Verification
Current static checks pass:
npm run lint
exited successfully
npx tsc --noEmit
exited successfully
The hold-cancel handoff was also adjusted based on runtime feedback so the cancel offset eases back more smoothly into the real timer progress.
#FilesChanged
Main file worked on:
app/(tabs)/timer.tsx
New note added:
notes/work-report-timer-2026-04-24.md
#Conclusion
The timer screen moved further toward a focused active-sprint experience. The countdown is now separated from the duration picker, task details have more visual weight, and cancel is treated as a deliberate hold action rather than a normal tap.
The main animation change is that hold-to-cancel now keeps the real timer progress separate from the temporary cancel speed-up effect. The code was also cleaned up so the timer flow is easier to read and continue working on.
Problems occuring after writing conclusion
Tried to implement sound by installing expo-audio. This caused the dependency list to update. The diff was massive, and something in the diff caused the entire timer page to break. Logic, animations - the lot. Have reverted back to last known working dependency list, as well as un-refactored a lot of code in an attempt to revert to a functioning state before figuring out that the culprit was dependencies. Need to figure our what is causing the critical failure in the new list.
Todo
- Re-refactor to make code cleaner, more readable and easier to maintain.
- Figure out the dependency issues of later dependency lists
Conclusion of dependecy saga
There was a mismatch in the nativewind dependency, with my one being ^4.2.3 and the other list being ^4.1.23. This cause my entire timer screen to fail. Animations got borked, buttons not working properly, duration picker only showing 2 indexes... the works. Solution - keepp nativewind dependency to ^4.2.3