@usefy/use-timer 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,492 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/geon0529/usefy/master/assets/logo.png" alt="usefy logo" width="120" />
3
+ </p>
4
+
5
+ <h1 align="center">@usefy/use-timer</h1>
6
+
7
+ <p align="center">
8
+ <strong>A powerful, accurate countdown timer hook for React applications</strong>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/@usefy/use-timer">
13
+ <img src="https://img.shields.io/npm/v/@usefy/use-timer.svg?style=flat-square&color=007acc" alt="npm version" />
14
+ </a>
15
+ <a href="https://www.npmjs.com/package/@usefy/use-timer">
16
+ <img src="https://img.shields.io/npm/dm/@usefy/use-timer.svg?style=flat-square&color=007acc" alt="npm downloads" />
17
+ </a>
18
+ <a href="https://bundlephobia.com/package/@usefy/use-timer">
19
+ <img src="https://img.shields.io/bundlephobia/minzip/@usefy/use-timer?style=flat-square&color=007acc" alt="bundle size" />
20
+ </a>
21
+ <a href="https://github.com/geon0529/usefy/blob/master/LICENSE">
22
+ <img src="https://img.shields.io/npm/l/@usefy/use-timer.svg?style=flat-square&color=007acc" alt="license" />
23
+ </a>
24
+ </p>
25
+
26
+ <p align="center">
27
+ <a href="#installation">Installation</a> •
28
+ <a href="#quick-start">Quick Start</a> •
29
+ <a href="#api-reference">API Reference</a> •
30
+ <a href="#examples">Examples</a> •
31
+ <a href="#license">License</a>
32
+ </p>
33
+
34
+ ---
35
+
36
+ ## Overview
37
+
38
+ `@usefy/use-timer` is a feature-rich countdown timer hook for React applications. It provides accurate time tracking with drift compensation, multiple time formats, flexible controls, and smart render optimization.
39
+
40
+ **Part of the [@usefy](https://www.npmjs.com/org/usefy) ecosystem** — a collection of production-ready React hooks designed for modern applications.
41
+
42
+ ### Why use-timer?
43
+
44
+ - **Accurate Timing** — Uses `performance.now()` with drift compensation for precise countdowns
45
+ - **Smart Render Optimization** — Only re-renders when the formatted time actually changes
46
+ - **Multiple Formats** — Built-in formats (`MM:SS`, `HH:MM:SS`, `mm:ss.SSS`) or custom formatters
47
+ - **Full Control** — Start, pause, stop, reset, restart, and toggle controls
48
+ - **Time Manipulation** — Add, subtract, or set time dynamically during countdown
49
+ - **Background Tab Support** — Visibility API integration for accurate time when switching tabs
50
+ - **Loop Mode** — Automatically restart the timer when it completes
51
+ - **TypeScript First** — Full type safety with comprehensive type definitions
52
+ - **Zero Dependencies** — Pure React implementation
53
+ - **Well Tested** — 123 tests with comprehensive coverage
54
+
55
+ ---
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ # npm
61
+ npm install @usefy/use-timer
62
+
63
+ # yarn
64
+ yarn add @usefy/use-timer
65
+
66
+ # pnpm
67
+ pnpm add @usefy/use-timer
68
+ ```
69
+
70
+ ### Peer Dependencies
71
+
72
+ This package requires React 18 or 19:
73
+
74
+ ```json
75
+ {
76
+ "peerDependencies": {
77
+ "react": "^18.0.0 || ^19.0.0"
78
+ }
79
+ }
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Quick Start
85
+
86
+ ```tsx
87
+ import { useTimer, ms } from "@usefy/use-timer";
88
+
89
+ function Timer() {
90
+ const timer = useTimer(ms.minutes(5), { format: "MM:SS" });
91
+
92
+ return (
93
+ <div>
94
+ <p>{timer.formattedTime}</p>
95
+ <button onClick={timer.toggle}>
96
+ {timer.isRunning ? "Pause" : "Start"}
97
+ </button>
98
+ <button onClick={timer.reset}>Reset</button>
99
+ </div>
100
+ );
101
+ }
102
+ ```
103
+
104
+ ---
105
+
106
+ ## API Reference
107
+
108
+ ### `useTimer(initialTimeMs, options?)`
109
+
110
+ A hook that manages countdown timer state with comprehensive controls.
111
+
112
+ #### Parameters
113
+
114
+ | Parameter | Type | Description |
115
+ | --------------- | ----------------- | -------------------------------------- |
116
+ | `initialTimeMs` | `number` | Initial countdown time in milliseconds |
117
+ | `options` | `UseTimerOptions` | Optional configuration object |
118
+
119
+ #### Options
120
+
121
+ | Option | Type | Default | Description |
122
+ | ------------ | ----------------------------- | --------- | ------------------------------------------------ |
123
+ | `interval` | `number` | `1` | Update interval in milliseconds |
124
+ | `format` | `TimeFormat \| TimeFormatter` | `"MM:SS"` | Time display format or custom formatter function |
125
+ | `autoStart` | `boolean` | `false` | Start timer automatically on mount |
126
+ | `loop` | `boolean` | `false` | Restart timer automatically when it completes |
127
+ | `useRAF` | `boolean` | `false` | Use `requestAnimationFrame` instead of interval |
128
+ | `onComplete` | `() => void` | - | Callback when timer reaches 0 |
129
+ | `onTick` | `(remaining: number) => void` | - | Callback on each tick with remaining time |
130
+ | `onStart` | `() => void` | - | Callback when timer starts |
131
+ | `onPause` | `() => void` | - | Callback when timer is paused |
132
+ | `onReset` | `() => void` | - | Callback when timer is reset |
133
+ | `onStop` | `() => void` | - | Callback when timer is stopped |
134
+
135
+ #### Time Formats
136
+
137
+ | Format | Example | Description |
138
+ | ---------------- | -------------- | ---------------------------------- |
139
+ | `"MM:SS"` | `05:30` | Minutes and seconds |
140
+ | `"HH:MM:SS"` | `01:05:30` | Hours, minutes, and seconds |
141
+ | `"SS"` | `330` | Total seconds |
142
+ | `"mm:ss.SSS"` | `05:30.123` | Minutes, seconds, and milliseconds |
143
+ | `"HH:MM:SS.SSS"` | `01:05:30.123` | Full format with milliseconds |
144
+ | Custom function | Any string | `(ms: number) => string` |
145
+
146
+ #### Returns `UseTimerReturn`
147
+
148
+ | Property | Type | Description |
149
+ | --------------- | ---------------------- | ------------------------------------------------------- |
150
+ | `time` | `number` | Current remaining time in milliseconds |
151
+ | `initialTime` | `number` | Initial time value |
152
+ | `formattedTime` | `string` | Formatted time string |
153
+ | `progress` | `number` | Progress percentage (0-100) |
154
+ | `status` | `TimerStatus` | Current status: `idle`, `running`, `paused`, `finished` |
155
+ | `isRunning` | `boolean` | Whether timer is running |
156
+ | `isPaused` | `boolean` | Whether timer is paused |
157
+ | `isFinished` | `boolean` | Whether timer has finished |
158
+ | `isIdle` | `boolean` | Whether timer is idle |
159
+ | `hours` | `number` | Decomposed hours component |
160
+ | `minutes` | `number` | Decomposed minutes component |
161
+ | `seconds` | `number` | Decomposed seconds component |
162
+ | `milliseconds` | `number` | Decomposed milliseconds component |
163
+ | `start` | `() => void` | Start the timer |
164
+ | `pause` | `() => void` | Pause the timer |
165
+ | `stop` | `() => void` | Stop timer (keep current time) |
166
+ | `reset` | `() => void` | Reset to initial time |
167
+ | `restart` | `() => void` | Reset and immediately start |
168
+ | `toggle` | `() => void` | Toggle between start/pause |
169
+ | `addTime` | `(ms: number) => void` | Add time to the timer |
170
+ | `subtractTime` | `(ms: number) => void` | Subtract time from the timer |
171
+ | `setTime` | `(ms: number) => void` | Set timer to a specific value |
172
+
173
+ ### `ms` Helper Object
174
+
175
+ A utility object for converting time units to milliseconds.
176
+
177
+ ```tsx
178
+ import { ms } from "@usefy/use-timer";
179
+
180
+ ms.seconds(30); // 30000
181
+ ms.minutes(5); // 300000
182
+ ms.hours(1); // 3600000
183
+
184
+ // Combine them
185
+ ms.hours(1) + ms.minutes(30); // 1h 30m = 5400000
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Examples
191
+
192
+ ### Basic Countdown Timer
193
+
194
+ ```tsx
195
+ import { useTimer, ms } from "@usefy/use-timer";
196
+
197
+ function CountdownTimer() {
198
+ const timer = useTimer(ms.minutes(5), {
199
+ format: "MM:SS",
200
+ onComplete: () => alert("Time's up!"),
201
+ });
202
+
203
+ return (
204
+ <div>
205
+ <div className="time-display">{timer.formattedTime}</div>
206
+ <div className="controls">
207
+ <button onClick={timer.toggle}>
208
+ {timer.isRunning ? "⏸ Pause" : "▶ Start"}
209
+ </button>
210
+ <button onClick={timer.reset}>↺ Reset</button>
211
+ </div>
212
+ </div>
213
+ );
214
+ }
215
+ ```
216
+
217
+ ### Precision Timer with Milliseconds
218
+
219
+ ```tsx
220
+ import { useTimer, ms } from "@usefy/use-timer";
221
+
222
+ function PrecisionTimer() {
223
+ const timer = useTimer(ms.seconds(10), {
224
+ format: "mm:ss.SSS",
225
+ interval: 1, // Update every 1ms
226
+ });
227
+
228
+ return (
229
+ <div>
230
+ <div className="precision-display">{timer.formattedTime}</div>
231
+ <button onClick={timer.toggle}>
232
+ {timer.isRunning ? "Pause" : "Start"}
233
+ </button>
234
+ </div>
235
+ );
236
+ }
237
+ ```
238
+
239
+ ### Auto-Start Timer
240
+
241
+ ```tsx
242
+ import { useTimer, ms } from "@usefy/use-timer";
243
+
244
+ function AutoStartTimer() {
245
+ const timer = useTimer(ms.seconds(30), {
246
+ autoStart: true,
247
+ format: "MM:SS",
248
+ onComplete: () => console.log("Completed!"),
249
+ });
250
+
251
+ return <div>{timer.formattedTime}</div>;
252
+ }
253
+ ```
254
+
255
+ ### Looping Timer
256
+
257
+ ```tsx
258
+ import { useTimer, ms } from "@usefy/use-timer";
259
+
260
+ function LoopingTimer() {
261
+ const timer = useTimer(ms.seconds(10), {
262
+ loop: true,
263
+ format: "MM:SS",
264
+ onComplete: () => console.log("Loop completed!"),
265
+ });
266
+
267
+ return (
268
+ <div>
269
+ <p>{timer.formattedTime}</p>
270
+ <button onClick={timer.toggle}>
271
+ {timer.isRunning ? "Pause" : "Start"}
272
+ </button>
273
+ </div>
274
+ );
275
+ }
276
+ ```
277
+
278
+ ### Time Manipulation
279
+
280
+ ```tsx
281
+ import { useTimer, ms } from "@usefy/use-timer";
282
+
283
+ function ManipulableTimer() {
284
+ const timer = useTimer(ms.minutes(1), { format: "MM:SS" });
285
+
286
+ return (
287
+ <div>
288
+ <p>{timer.formattedTime}</p>
289
+ <div className="controls">
290
+ <button onClick={timer.toggle}>
291
+ {timer.isRunning ? "Pause" : "Start"}
292
+ </button>
293
+ <button onClick={() => timer.addTime(ms.seconds(10))}>+10s</button>
294
+ <button onClick={() => timer.subtractTime(ms.seconds(10))}>-10s</button>
295
+ <button onClick={timer.reset}>Reset</button>
296
+ </div>
297
+ </div>
298
+ );
299
+ }
300
+ ```
301
+
302
+ ### Progress Bar
303
+
304
+ ```tsx
305
+ import { useTimer, ms } from "@usefy/use-timer";
306
+
307
+ function TimerWithProgress() {
308
+ const timer = useTimer(ms.minutes(1), { format: "MM:SS" });
309
+
310
+ return (
311
+ <div>
312
+ <p>{timer.formattedTime}</p>
313
+ <div className="progress-bar">
314
+ <div
315
+ className="progress-fill"
316
+ style={{ width: `${100 - timer.progress}%` }}
317
+ />
318
+ </div>
319
+ <button onClick={timer.toggle}>
320
+ {timer.isRunning ? "Pause" : "Start"}
321
+ </button>
322
+ </div>
323
+ );
324
+ }
325
+ ```
326
+
327
+ ### Custom Formatter
328
+
329
+ ```tsx
330
+ import { useTimer, ms } from "@usefy/use-timer";
331
+
332
+ function CustomFormattedTimer() {
333
+ const timer = useTimer(ms.hours(2) + ms.minutes(30), {
334
+ format: (ms) => {
335
+ const hours = Math.floor(ms / 3600000);
336
+ const minutes = Math.floor((ms % 3600000) / 60000);
337
+ const seconds = Math.floor((ms % 60000) / 1000);
338
+ return `${hours}h ${minutes}m ${seconds}s`;
339
+ },
340
+ });
341
+
342
+ return (
343
+ <div>
344
+ <p>{timer.formattedTime}</p> {/* "2h 30m 0s" */}
345
+ <button onClick={timer.toggle}>
346
+ {timer.isRunning ? "Pause" : "Start"}
347
+ </button>
348
+ </div>
349
+ );
350
+ }
351
+ ```
352
+
353
+ ### Kitchen Timer with Presets
354
+
355
+ ```tsx
356
+ import { useTimer, ms } from "@usefy/use-timer";
357
+
358
+ function KitchenTimer() {
359
+ const timer = useTimer(ms.minutes(5), {
360
+ format: "MM:SS",
361
+ onComplete: () => playAlarm(),
362
+ });
363
+
364
+ const presets = [1, 3, 5, 10, 15, 30];
365
+
366
+ return (
367
+ <div>
368
+ <div className="presets">
369
+ {presets.map((min) => (
370
+ <button key={min} onClick={() => timer.setTime(ms.minutes(min))}>
371
+ {min}m
372
+ </button>
373
+ ))}
374
+ </div>
375
+ <p>{timer.formattedTime}</p>
376
+ <button onClick={timer.toggle}>
377
+ {timer.isRunning ? "Pause" : "Start"}
378
+ </button>
379
+ </div>
380
+ );
381
+ }
382
+ ```
383
+
384
+ ---
385
+
386
+ ## Render Optimization
387
+
388
+ The hook is optimized to **only trigger re-renders when the formatted time changes**, not on every interval tick. This means:
389
+
390
+ | Format | Re-render frequency |
391
+ | ----------- | ------------------- |
392
+ | `HH:MM` | Every minute |
393
+ | `MM:SS` | Every second |
394
+ | `MM:SS.S` | Every 100ms |
395
+ | `MM:SS.SS` | Every 10ms |
396
+ | `MM:SS.SSS` | Every 1ms |
397
+
398
+ This optimization is automatic — no configuration needed!
399
+
400
+ ---
401
+
402
+ ## TypeScript
403
+
404
+ This hook is written in TypeScript and exports comprehensive type definitions.
405
+
406
+ ```tsx
407
+ import {
408
+ useTimer,
409
+ ms,
410
+ type UseTimerOptions,
411
+ type UseTimerReturn,
412
+ type TimerStatus,
413
+ type TimeFormat,
414
+ type TimeFormatter,
415
+ } from "@usefy/use-timer";
416
+
417
+ // Full type inference
418
+ const timer: UseTimerReturn = useTimer(60000, {
419
+ format: "MM:SS",
420
+ onComplete: () => console.log("Done!"),
421
+ });
422
+
423
+ // Status is typed as union
424
+ const status: TimerStatus = timer.status; // "idle" | "running" | "paused" | "finished"
425
+ ```
426
+
427
+ ---
428
+
429
+ ## Performance
430
+
431
+ - **Stable Function References** — All control functions are memoized with `useCallback`
432
+ - **Smart Re-renders** — Only re-renders when formatted time changes
433
+ - **Drift Compensation** — Uses `performance.now()` for accurate timing
434
+ - **Background Tab Handling** — Visibility API integration prevents timer drift when tab is inactive
435
+
436
+ ```tsx
437
+ const { start, pause, toggle, reset } = useTimer(60000);
438
+
439
+ // These references remain stable across renders
440
+ useEffect(() => {
441
+ // Safe to use as dependencies
442
+ }, [start, pause, toggle, reset]);
443
+ ```
444
+
445
+ ---
446
+
447
+ ## Testing
448
+
449
+ This package maintains comprehensive test coverage to ensure reliability and stability.
450
+
451
+ ### Test Coverage
452
+
453
+ | File | Statements | Branches | Functions | Lines |
454
+ | ---------------- | ---------- | -------- | --------- | ------ |
455
+ | `useTimer.ts` | 84.11% | 72.63% | 94.11% | 84.36% |
456
+ | `formatUtils.ts` | 100% | 100% | 100% | 100% |
457
+ | `timeUtils.ts` | 100% | 100% | 100% | 100% |
458
+
459
+ ### Test Files
460
+
461
+ - `useTimer.test.ts` — 67 tests for hook behavior
462
+ - `formatUtils.test.ts` — 24 tests for time formatting
463
+ - `timeUtils.test.ts` — 32 tests for time utilities
464
+
465
+ **Total: 123 tests**
466
+
467
+ ### Running Tests
468
+
469
+ ```bash
470
+ # Run all tests
471
+ pnpm test
472
+
473
+ # Run tests in watch mode
474
+ pnpm test:watch
475
+
476
+ # Run tests with coverage report
477
+ pnpm test --coverage
478
+ ```
479
+
480
+ ---
481
+
482
+ ## License
483
+
484
+ MIT © [mirunamu](https://github.com/geon0529)
485
+
486
+ This package is part of the [usefy](https://github.com/geon0529/usefy) monorepo.
487
+
488
+ ---
489
+
490
+ <p align="center">
491
+ <sub>Built with care by the usefy team</sub>
492
+ </p>