@@ -37,7 +37,7 @@ const PomodoroTimer: React.FC<PomodoroTimerProps> = ({
3737 const {
3838 isRunning,
3939 isPaused,
40- remainingTime,
40+ remainingTime : storeRemainingTime , // Rename to distinguish from local smooth state
4141 mode,
4242 start,
4343 pause,
@@ -50,7 +50,8 @@ const PomodoroTimer: React.FC<PomodoroTimerProps> = ({
5050 breakDuration,
5151 setDurations,
5252 setStartTime,
53- startTime,
53+ startTime : storeStartTime ,
54+ endTime, // Use endTime for smooth calcs
5455 } = usePomodoroStore ( ) ;
5556 const { pomodoro, updatePomodoro, theme } = useSettings ( ) ;
5657 const { addSession } = usePomodoroHistory ( ) ;
@@ -73,6 +74,10 @@ const PomodoroTimer: React.FC<PomodoroTimerProps> = ({
7374 } ) ;
7475 const offsetRef = useRef ( { x : 0 , y : 0 } ) ;
7576
77+ // Local state for smooth updates
78+ const [ smoothRemaining , setSmoothRemaining ] = useState ( storeRemainingTime ) ;
79+ const [ smoothProgress , setSmoothProgress ] = useState ( 0 ) ;
80+
7681 useEffect ( ( ) => {
7782 try {
7883 localStorage . setItem ( "pomodoroFloatPos" , JSON . stringify ( position ) ) ;
@@ -109,6 +114,46 @@ const PomodoroTimer: React.FC<PomodoroTimerProps> = ({
109114 window . addEventListener ( "pointerup" , stopDrag ) ;
110115 } ;
111116
117+ // High frequency update loop for smooth visuals
118+ useEffect ( ( ) => {
119+ if ( ! isRunning || isPaused || ! endTime ) {
120+ // Fallback to store values when not running smoothly
121+ setSmoothRemaining ( storeRemainingTime ) ;
122+ const duration = mode === "work" ? workDuration : breakDuration ;
123+ setSmoothProgress ( storeRemainingTime / duration ) ;
124+ return ;
125+ }
126+
127+ let animationFrameId : number ;
128+
129+ const animate = ( ) => {
130+ const now = Date . now ( ) ;
131+ const msRemaining = Math . max ( 0 , endTime - now ) ;
132+ const secondsRemaining = Math . ceil ( msRemaining / 1000 ) ;
133+
134+ const duration = mode === "work" ? workDuration : breakDuration ;
135+ // Calculate precise progress (0.0 to 1.0)
136+ // We use ms for the ring to be buttery smooth
137+ // Total duration in ms
138+ const durationMs = duration * 1000 ;
139+ // Progress acts inverted in the original code (remaining / total)
140+ const exactProgress = Math . min ( 1 , Math . max ( 0 , msRemaining / durationMs ) ) ;
141+
142+ setSmoothRemaining ( secondsRemaining ) ;
143+ setSmoothProgress ( exactProgress ) ;
144+
145+ if ( msRemaining > 0 ) {
146+ animationFrameId = requestAnimationFrame ( animate ) ;
147+ }
148+ } ;
149+
150+ animate ( ) ;
151+
152+ return ( ) => cancelAnimationFrame ( animationFrameId ) ;
153+ } , [ isRunning , isPaused , endTime , mode , workDuration , breakDuration , storeRemainingTime ] ) ;
154+
155+
156+ // Keep 'now' updated for pause duration display (low freq is fine for this)
112157 useEffect ( ( ) => {
113158 if ( ! isPaused ) return ;
114159 const i = setInterval ( ( ) => setNow ( Date . now ( ) ) , 1000 ) ;
@@ -189,16 +234,16 @@ const PomodoroTimer: React.FC<PomodoroTimerProps> = ({
189234 // We rely on store.startTime for both work and break now.
190235
191236 const handlePause = ( ) => {
192- if ( startTime ) {
193- addSession ( startTime , Date . now ( ) , mode ) ;
237+ if ( storeStartTime ) {
238+ addSession ( storeStartTime , Date . now ( ) , mode ) ;
194239 setStartTime ( undefined ) ;
195240 }
196241 pause ( ) ;
197242 } ;
198243
199244 const handleReset = ( ) => {
200- if ( startTime ) {
201- addSession ( startTime , Date . now ( ) , mode ) ;
245+ if ( storeStartTime ) {
246+ addSession ( storeStartTime , Date . now ( ) , mode ) ;
202247 }
203248 setStartTime ( undefined ) ;
204249 reset ( ) ;
@@ -209,13 +254,13 @@ const PomodoroTimer: React.FC<PomodoroTimerProps> = ({
209254 addSession ( pauseStart , Date . now ( ) , "break" ) ;
210255 }
211256 resume ( ) ;
212- setStartTime ( Date . now ( ) ) ;
257+ // Resume function in store now handles startTime/endTime logic
213258 } ;
214259
215260 const handleStartBreak = ( ) => {
216261 // If we are currently working, save the work done so far
217- if ( mode === "work" && startTime ) {
218- addSession ( startTime , Date . now ( ) , "work" ) ;
262+ if ( mode === "work" && storeStartTime ) {
263+ addSession ( storeStartTime , Date . now ( ) , "work" ) ;
219264 } else if ( pauseStart ) {
220265 // If we were paused, the gap was a break
221266 addSession ( pauseStart , Date . now ( ) , "break" ) ;
@@ -226,8 +271,8 @@ const PomodoroTimer: React.FC<PomodoroTimerProps> = ({
226271
227272 const handleSkipBreak = ( ) => {
228273 // If we are currently in a break, save the break time taken so far
229- if ( mode === "break" && startTime ) {
230- addSession ( startTime , Date . now ( ) , "break" ) ;
274+ if ( mode === "break" && storeStartTime ) {
275+ addSession ( storeStartTime , Date . now ( ) , "break" ) ;
231276 } else if ( pauseStart ) {
232277 addSession ( pauseStart , Date . now ( ) , "break" ) ;
233278 }
@@ -296,14 +341,19 @@ const PomodoroTimer: React.FC<PomodoroTimerProps> = ({
296341
297342 if ( compact && ! isRunning ) return null ;
298343
299- const duration = mode === "work" ? workDuration : breakDuration ;
300- const progress = remainingTime / duration ;
301-
302344 const radius = size ;
303345 const stroke = 8 ;
304346 const normalizedRadius = radius - stroke / 2 ;
305347 const circumference = normalizedRadius * 2 * Math . PI ;
306- const strokeDashoffset = circumference - progress * circumference ;
348+
349+ // Use smoothProgress for ring, smoothRemaining for text
350+ // smoothProgress is 0..1 (remaining/total), but we want inverse for strokeDashoffset calc if we want it to shrink
351+ // The original code was: progress = remainingTime / duration
352+ // strokeDashoffset = circumference - progress * circumference
353+
354+ // So if progress is 1 (full), offset is 0 (full ring).
355+ // If progress is 0 (empty), offset is circumference (empty ring).
356+ const strokeDashoffset = circumference - smoothProgress * circumference ;
307357
308358 return (
309359 < div
@@ -344,7 +394,7 @@ const PomodoroTimer: React.FC<PomodoroTimerProps> = ({
344394 strokeDasharray = { `${ circumference } ${ circumference } ` }
345395 style = { {
346396 strokeDashoffset,
347- transition : "stroke-dashoffset 1s linear" ,
397+ transition : "stroke-dashoffset 0s linear" , // Disable CSS transition for smooth JS animation
348398 } }
349399 r = { normalizedRadius }
350400 cx = { radius }
@@ -357,7 +407,7 @@ const PomodoroTimer: React.FC<PomodoroTimerProps> = ({
357407 >
358408 { isPaused
359409 ? `${ t ( "pomodoroTimer.pauseLabel" ) } ${ formatTime ( pauseDuration ) } `
360- : formatTime ( remainingTime ) }
410+ : formatTime ( smoothRemaining ) }
361411 </ div >
362412 </ div >
363413 </ div >
0 commit comments