@react-chess-tools/react-chess-clock 1.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +697 -0
  3. package/dist/index.cjs +1014 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +528 -0
  6. package/dist/index.d.ts +528 -0
  7. package/dist/index.js +969 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +63 -0
  10. package/src/components/ChessClock/ChessClock.stories.tsx +782 -0
  11. package/src/components/ChessClock/index.ts +44 -0
  12. package/src/components/ChessClock/parts/Display.tsx +69 -0
  13. package/src/components/ChessClock/parts/PlayPause.tsx +190 -0
  14. package/src/components/ChessClock/parts/Reset.tsx +90 -0
  15. package/src/components/ChessClock/parts/Root.tsx +37 -0
  16. package/src/components/ChessClock/parts/Switch.tsx +84 -0
  17. package/src/components/ChessClock/parts/__tests__/Display.test.tsx +149 -0
  18. package/src/components/ChessClock/parts/__tests__/PlayPause.test.tsx +411 -0
  19. package/src/components/ChessClock/parts/__tests__/Reset.test.tsx +160 -0
  20. package/src/components/ChessClock/parts/__tests__/Root.test.tsx +49 -0
  21. package/src/components/ChessClock/parts/__tests__/Switch.test.tsx +204 -0
  22. package/src/hooks/__tests__/clockReducer.test.ts +985 -0
  23. package/src/hooks/__tests__/useChessClock.test.tsx +1080 -0
  24. package/src/hooks/clockReducer.ts +379 -0
  25. package/src/hooks/useChessClock.ts +406 -0
  26. package/src/hooks/useChessClockContext.ts +35 -0
  27. package/src/index.ts +65 -0
  28. package/src/types.ts +217 -0
  29. package/src/utils/__tests__/calculateSwitchTime.test.ts +150 -0
  30. package/src/utils/__tests__/formatTime.test.ts +83 -0
  31. package/src/utils/__tests__/timeControl.test.ts +414 -0
  32. package/src/utils/__tests__/timingMethods.test.ts +170 -0
  33. package/src/utils/calculateSwitchTime.ts +37 -0
  34. package/src/utils/formatTime.ts +59 -0
  35. package/src/utils/presets.ts +47 -0
  36. package/src/utils/timeControl.ts +205 -0
  37. package/src/utils/timingMethods.ts +103 -0
package/dist/index.js ADDED
@@ -0,0 +1,969 @@
1
+ // src/components/ChessClock/parts/Root.tsx
2
+ import React from "react";
3
+
4
+ // src/hooks/useChessClockContext.ts
5
+ import { createContext, useContext } from "react";
6
+ var ChessClockContext = createContext(
7
+ null
8
+ );
9
+ function useChessClockContext() {
10
+ const context = useContext(ChessClockContext);
11
+ if (!context) {
12
+ throw new Error(
13
+ "useChessClockContext must be used within a ChessClock.Root component. Make sure your component is wrapped with <ChessClock.Root>."
14
+ );
15
+ }
16
+ return context;
17
+ }
18
+
19
+ // src/hooks/useChessClock.ts
20
+ import {
21
+ useCallback,
22
+ useEffect,
23
+ useMemo,
24
+ useReducer,
25
+ useRef,
26
+ useState
27
+ } from "react";
28
+
29
+ // src/utils/timeControl.ts
30
+ var TIME_CONTROL_REGEX = /^(\d+(?:\.\d+)?)(?:\+(\d+(?:\.\d+)?))?$/;
31
+ var PERIOD_REGEX = /^(?:(?:(\d+)|sd|g)\/)?(\d+(?:\.\d+)?)(?:\+(\d+(?:\.\d+)?))?$/i;
32
+ function parseTimeControlString(input) {
33
+ const match = input.match(TIME_CONTROL_REGEX);
34
+ if (!match) {
35
+ throw new Error(
36
+ `Invalid time control string: "${input}". Expected format: "5+3" (minutes + increment) or "10" (minutes only)`
37
+ );
38
+ }
39
+ const minutes = parseFloat(match[1]);
40
+ const increment = match[2] ? parseFloat(match[2]) : 0;
41
+ return {
42
+ baseTime: Math.round(minutes * 60),
43
+ increment: Math.round(increment)
44
+ };
45
+ }
46
+ function parsePeriod(periodStr) {
47
+ const trimmed = periodStr.trim();
48
+ const match = trimmed.match(PERIOD_REGEX);
49
+ if (!match) {
50
+ throw new Error(
51
+ `Invalid period format: "${trimmed}". Expected format: "40/90+30" or "sd/30+30"`
52
+ );
53
+ }
54
+ const [, movesStr, timeStr, incrementStr] = match;
55
+ const moves = movesStr ? parseInt(movesStr, 10) : void 0;
56
+ const minutes = parseFloat(timeStr);
57
+ const increment = incrementStr ? parseFloat(incrementStr) : 0;
58
+ return {
59
+ baseTime: Math.round(minutes * 60),
60
+ increment: increment > 0 ? Math.round(increment) : void 0,
61
+ moves
62
+ };
63
+ }
64
+ function parseMultiPeriodTimeControl(input) {
65
+ const parts = input.split(/\s*,\s*/);
66
+ return parts.map((part) => parsePeriod(part));
67
+ }
68
+ function normalizeTimeControl(input, timingMethod, clockStart) {
69
+ if (Array.isArray(input)) {
70
+ if (input.length === 0) {
71
+ throw new Error(
72
+ "Multi-period time control must have at least one period"
73
+ );
74
+ }
75
+ const periods = input.map((period) => ({
76
+ ...period,
77
+ baseTime: period.baseTime * 1e3,
78
+ increment: period.increment !== void 0 ? period.increment * 1e3 : void 0,
79
+ delay: period.delay !== void 0 ? period.delay * 1e3 : void 0
80
+ }));
81
+ const lastPeriod = periods[periods.length - 1];
82
+ if (lastPeriod.moves !== void 0) {
83
+ lastPeriod.moves = void 0;
84
+ }
85
+ const firstPeriod = periods[0];
86
+ return {
87
+ baseTime: firstPeriod.baseTime,
88
+ increment: firstPeriod.increment ?? 0,
89
+ delay: firstPeriod.delay ?? 0,
90
+ timingMethod,
91
+ clockStart,
92
+ periods
93
+ };
94
+ }
95
+ let parsed;
96
+ if (typeof input === "string") {
97
+ parsed = parseTimeControlString(input);
98
+ } else {
99
+ parsed = input;
100
+ }
101
+ return {
102
+ baseTime: parsed.baseTime * 1e3,
103
+ // Convert to milliseconds
104
+ increment: (parsed.increment ?? 0) * 1e3,
105
+ // Convert to milliseconds
106
+ delay: (parsed.delay ?? 0) * 1e3,
107
+ // Convert to milliseconds
108
+ timingMethod,
109
+ clockStart
110
+ };
111
+ }
112
+ function parseTimeControlConfig(config) {
113
+ const normalized = normalizeTimeControl(
114
+ config.time,
115
+ config.timingMethod ?? "fischer",
116
+ config.clockStart ?? "delayed"
117
+ );
118
+ return {
119
+ ...normalized,
120
+ whiteTimeOverride: config.whiteTime ? config.whiteTime * 1e3 : void 0,
121
+ blackTimeOverride: config.blackTime ? config.blackTime * 1e3 : void 0
122
+ };
123
+ }
124
+ function getInitialTimes(config) {
125
+ return {
126
+ white: config.whiteTimeOverride ?? config.baseTime,
127
+ black: config.blackTimeOverride ?? config.baseTime
128
+ };
129
+ }
130
+
131
+ // src/utils/formatTime.ts
132
+ var AUTO_FORMAT_THRESHOLD = 20;
133
+ function formatClockTime(milliseconds, format = "auto") {
134
+ const clampedMs = Math.max(0, milliseconds);
135
+ if (format === "auto") {
136
+ format = clampedMs < AUTO_FORMAT_THRESHOLD * 1e3 ? "ss.d" : "mm:ss";
137
+ }
138
+ const totalSeconds = Math.ceil(clampedMs / 1e3);
139
+ switch (format) {
140
+ case "ss.d": {
141
+ const secondsWithDecimal = clampedMs / 1e3;
142
+ return secondsWithDecimal.toFixed(1);
143
+ }
144
+ case "mm:ss": {
145
+ const totalMinutes = Math.floor(totalSeconds / 60);
146
+ const seconds = totalSeconds % 60;
147
+ if (totalMinutes >= 60) {
148
+ const hours = Math.floor(totalMinutes / 60);
149
+ const minutes = totalMinutes % 60;
150
+ return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
151
+ }
152
+ return `${totalMinutes}:${seconds.toString().padStart(2, "0")}`;
153
+ }
154
+ case "hh:mm:ss": {
155
+ const hours = Math.floor(totalSeconds / 3600);
156
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
157
+ const seconds = totalSeconds % 60;
158
+ return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
159
+ }
160
+ default:
161
+ return String(totalSeconds);
162
+ }
163
+ }
164
+
165
+ // src/utils/timingMethods.ts
166
+ function applyFischerIncrement(currentTime, increment) {
167
+ return currentTime + increment;
168
+ }
169
+ function applyBronsteinDelay(currentTime, timeSpent, delay) {
170
+ const addBack = Math.min(timeSpent, delay);
171
+ return currentTime + addBack;
172
+ }
173
+ function calculateSwitchAdjustment(timingMethod, currentTime, timeSpent, config) {
174
+ switch (timingMethod) {
175
+ case "fischer":
176
+ return applyFischerIncrement(currentTime, config.increment);
177
+ case "delay":
178
+ return currentTime;
179
+ case "bronstein":
180
+ return applyBronsteinDelay(currentTime, timeSpent, config.delay);
181
+ default:
182
+ return currentTime;
183
+ }
184
+ }
185
+ function getInitialActivePlayer(clockStart) {
186
+ return clockStart === "immediate" ? "white" : null;
187
+ }
188
+ function getInitialStatus(clockStart) {
189
+ if (clockStart === "immediate") return "running";
190
+ if (clockStart === "delayed") return "delayed";
191
+ return "idle";
192
+ }
193
+
194
+ // src/utils/calculateSwitchTime.ts
195
+ function calculateSwitchTime(currentTime, timeSpent, config) {
196
+ let effectiveElapsed = timeSpent;
197
+ if (config.timingMethod === "delay") {
198
+ effectiveElapsed = Math.max(0, timeSpent - config.delay);
199
+ }
200
+ const newTime = Math.max(0, currentTime - effectiveElapsed);
201
+ return calculateSwitchAdjustment(
202
+ config.timingMethod,
203
+ newTime,
204
+ timeSpent,
205
+ config
206
+ );
207
+ }
208
+
209
+ // src/hooks/clockReducer.ts
210
+ function maybeAdvancePeriod(times, periodState) {
211
+ let newTimes = times;
212
+ let newPeriodState = periodState;
213
+ ["white", "black"].forEach((player) => {
214
+ const playerPeriodIndex = newPeriodState.periodIndex[player];
215
+ if (playerPeriodIndex < 0 || playerPeriodIndex >= newPeriodState.periods.length) {
216
+ return;
217
+ }
218
+ const currentPeriod = newPeriodState.periods[playerPeriodIndex];
219
+ if (!(currentPeriod == null ? void 0 : currentPeriod.moves)) return;
220
+ const movesRequired = currentPeriod.moves;
221
+ const playerMoves = newPeriodState.periodMoves[player];
222
+ if (playerMoves >= movesRequired) {
223
+ const nextPeriodIndex = playerPeriodIndex + 1;
224
+ if (nextPeriodIndex >= newPeriodState.periods.length) {
225
+ return;
226
+ }
227
+ const nextPeriod = newPeriodState.periods[nextPeriodIndex];
228
+ if (!nextPeriod) return;
229
+ const addedTime = nextPeriod.baseTime;
230
+ newPeriodState = {
231
+ ...newPeriodState,
232
+ periodIndex: {
233
+ ...newPeriodState.periodIndex,
234
+ [player]: nextPeriodIndex
235
+ },
236
+ periodMoves: {
237
+ ...newPeriodState.periodMoves,
238
+ [player]: 0
239
+ }
240
+ };
241
+ newTimes = {
242
+ ...newTimes,
243
+ [player]: newTimes[player] + addedTime
244
+ };
245
+ }
246
+ });
247
+ return { times: newTimes, periodState: newPeriodState };
248
+ }
249
+ function clockReducer(state, action) {
250
+ var _a, _b, _c;
251
+ switch (action.type) {
252
+ case "START": {
253
+ if (state.status === "finished") return state;
254
+ if (state.status === "delayed") {
255
+ return state;
256
+ }
257
+ const now = ((_a = action.payload) == null ? void 0 : _a.now) ?? Date.now();
258
+ return {
259
+ ...state,
260
+ status: "running",
261
+ activePlayer: state.activePlayer ?? "white",
262
+ // Initialize move start time if not already set
263
+ moveStartTime: state.moveStartTime ?? now
264
+ };
265
+ }
266
+ case "PAUSE": {
267
+ if (state.status !== "running") return state;
268
+ const now = ((_b = action.payload) == null ? void 0 : _b.now) ?? Date.now();
269
+ const elapsedAtPause = state.moveStartTime !== null ? now - state.moveStartTime : 0;
270
+ return {
271
+ ...state,
272
+ status: "paused",
273
+ elapsedAtPause,
274
+ moveStartTime: null
275
+ };
276
+ }
277
+ case "RESUME": {
278
+ if (state.status !== "paused") return state;
279
+ const now = ((_c = action.payload) == null ? void 0 : _c.now) ?? Date.now();
280
+ return {
281
+ ...state,
282
+ status: "running",
283
+ moveStartTime: now - state.elapsedAtPause,
284
+ elapsedAtPause: 0
285
+ };
286
+ }
287
+ case "SWITCH": {
288
+ if (state.status === "finished") return state;
289
+ let newPeriodState = state.periodState;
290
+ const movedPlayer = state.activePlayer;
291
+ const now = action.payload.now ?? Date.now();
292
+ if (state.status === "delayed") {
293
+ const newCount = state.switchCount + 1;
294
+ const newPlayer2 = newCount % 2 === 1 ? "black" : "white";
295
+ const delayedMovePlayer = newCount % 2 === 1 ? "white" : "black";
296
+ if (state.periodState) {
297
+ newPeriodState = {
298
+ ...state.periodState,
299
+ periodMoves: {
300
+ ...state.periodState.periodMoves,
301
+ [delayedMovePlayer]: state.periodState.periodMoves[delayedMovePlayer] + 1
302
+ }
303
+ };
304
+ }
305
+ if (newPeriodState) {
306
+ const advanced = maybeAdvancePeriod(state.times, newPeriodState);
307
+ newPeriodState = advanced.periodState;
308
+ }
309
+ const newStateStatus = newCount >= 2 ? "running" : "delayed";
310
+ return {
311
+ ...state,
312
+ activePlayer: newPlayer2,
313
+ switchCount: newCount,
314
+ status: newStateStatus,
315
+ periodState: newPeriodState,
316
+ // Start timing when transitioning to running
317
+ moveStartTime: newStateStatus === "running" ? now : state.moveStartTime
318
+ };
319
+ }
320
+ if (movedPlayer === null) return state;
321
+ const { newTimes } = action.payload;
322
+ const newPlayer = movedPlayer === "white" ? "black" : "white";
323
+ if (state.periodState) {
324
+ newPeriodState = {
325
+ ...state.periodState,
326
+ periodMoves: {
327
+ ...state.periodState.periodMoves,
328
+ [movedPlayer]: state.periodState.periodMoves[movedPlayer] + 1
329
+ }
330
+ };
331
+ }
332
+ let resolvedTimes = newTimes ?? state.times;
333
+ if (newPeriodState) {
334
+ const advanced = maybeAdvancePeriod(resolvedTimes, newPeriodState);
335
+ resolvedTimes = advanced.times;
336
+ newPeriodState = advanced.periodState;
337
+ }
338
+ const newStatus = state.status === "idle" ? "running" : state.status;
339
+ return {
340
+ ...state,
341
+ activePlayer: newPlayer,
342
+ times: resolvedTimes,
343
+ switchCount: state.switchCount + 1,
344
+ periodState: newPeriodState,
345
+ status: newStatus,
346
+ // Reset timing for the new player
347
+ moveStartTime: newStatus === "running" ? now : null,
348
+ elapsedAtPause: 0
349
+ };
350
+ }
351
+ case "TIMEOUT": {
352
+ const { player } = action.payload;
353
+ return {
354
+ ...state,
355
+ status: "finished",
356
+ timeout: player,
357
+ times: { ...state.times, [player]: 0 }
358
+ };
359
+ }
360
+ case "RESET": {
361
+ const config = parseTimeControlConfig(action.payload);
362
+ const initialTimes = getInitialTimes(config);
363
+ const now = action.payload.now;
364
+ const periodState = config.periods ? {
365
+ periodIndex: { white: 0, black: 0 },
366
+ periodMoves: { white: 0, black: 0 },
367
+ periods: config.periods
368
+ } : void 0;
369
+ return createInitialClockState(
370
+ initialTimes,
371
+ getInitialStatus(config.clockStart),
372
+ getInitialActivePlayer(config.clockStart),
373
+ config,
374
+ periodState,
375
+ now
376
+ );
377
+ }
378
+ case "ADD_TIME": {
379
+ const { player, milliseconds } = action.payload;
380
+ const now = action.payload.now ?? Date.now();
381
+ const newTimes = {
382
+ ...state.times,
383
+ [player]: state.times[player] + milliseconds
384
+ };
385
+ const resetElapsed = state.status === "paused" && player === state.activePlayer;
386
+ return {
387
+ ...state,
388
+ times: newTimes,
389
+ moveStartTime: state.status === "running" ? now : null,
390
+ ...resetElapsed && { elapsedAtPause: 0 }
391
+ };
392
+ }
393
+ case "SET_TIME": {
394
+ const { player, milliseconds } = action.payload;
395
+ const now = action.payload.now ?? Date.now();
396
+ const newTimes = {
397
+ ...state.times,
398
+ [player]: Math.max(0, milliseconds)
399
+ };
400
+ const resetElapsed = state.status === "paused" && player === state.activePlayer;
401
+ return {
402
+ ...state,
403
+ times: newTimes,
404
+ moveStartTime: state.status === "running" ? now : null,
405
+ ...resetElapsed && { elapsedAtPause: 0 }
406
+ };
407
+ }
408
+ default:
409
+ return state;
410
+ }
411
+ }
412
+ function createInitialClockState(initialTimes, status, activePlayer, config, periodState, now) {
413
+ return {
414
+ times: initialTimes,
415
+ initialTimes,
416
+ status,
417
+ activePlayer,
418
+ timeout: null,
419
+ switchCount: 0,
420
+ periodState,
421
+ config,
422
+ // If starting immediately, initialize the move start time
423
+ moveStartTime: status === "running" ? now ?? Date.now() : null,
424
+ elapsedAtPause: 0
425
+ };
426
+ }
427
+
428
+ // src/hooks/useChessClock.ts
429
+ var DISABLED_CLOCK_CONFIG = {
430
+ time: { baseTime: 0 }
431
+ };
432
+ function serializeTimeRelevantConfig(config) {
433
+ return JSON.stringify({
434
+ time: config.time,
435
+ timingMethod: config.timingMethod,
436
+ clockStart: config.clockStart,
437
+ whiteTime: config.whiteTime,
438
+ blackTime: config.blackTime
439
+ });
440
+ }
441
+ function calculateDisplayTime(baseTime, moveStartTime, elapsedAtPause, timingMethod, delay) {
442
+ if (moveStartTime === null) {
443
+ let effectiveElapsed2 = elapsedAtPause;
444
+ if (timingMethod === "delay") {
445
+ effectiveElapsed2 = Math.max(0, elapsedAtPause - delay);
446
+ }
447
+ return Math.max(0, baseTime - effectiveElapsed2);
448
+ }
449
+ const now = Date.now();
450
+ const rawElapsed = now - moveStartTime;
451
+ let effectiveElapsed = rawElapsed;
452
+ if (timingMethod === "delay") {
453
+ effectiveElapsed = Math.max(0, rawElapsed - delay);
454
+ }
455
+ return Math.max(0, baseTime - effectiveElapsed);
456
+ }
457
+ function useChessClock(options) {
458
+ const [state, dispatch] = useReducer(clockReducer, null, () => {
459
+ const initialConfig = parseTimeControlConfig(options);
460
+ const initialTimesValue = getInitialTimes(initialConfig);
461
+ const initialPeriodState = initialConfig.periods && initialConfig.periods.length > 1 ? {
462
+ periodIndex: { white: 0, black: 0 },
463
+ periodMoves: { white: 0, black: 0 },
464
+ periods: initialConfig.periods
465
+ } : void 0;
466
+ return createInitialClockState(
467
+ initialTimesValue,
468
+ getInitialStatus(initialConfig.clockStart),
469
+ getInitialActivePlayer(initialConfig.clockStart),
470
+ initialConfig,
471
+ initialPeriodState
472
+ );
473
+ });
474
+ const optionsRef = useRef(options);
475
+ optionsRef.current = options;
476
+ const stateRef = useRef(state);
477
+ stateRef.current = state;
478
+ const configKey = serializeTimeRelevantConfig(options);
479
+ const prevConfigRef = useRef(configKey);
480
+ useEffect(() => {
481
+ if (prevConfigRef.current !== configKey) {
482
+ prevConfigRef.current = configKey;
483
+ dispatch({ type: "RESET", payload: { ...options, now: Date.now() } });
484
+ }
485
+ }, [configKey, options]);
486
+ const [tick, forceUpdate] = useState(0);
487
+ const displayTimes = state.activePlayer === null ? state.times : {
488
+ ...state.times,
489
+ [state.activePlayer]: calculateDisplayTime(
490
+ state.times[state.activePlayer],
491
+ state.moveStartTime,
492
+ state.elapsedAtPause,
493
+ state.config.timingMethod,
494
+ state.config.delay
495
+ )
496
+ };
497
+ const activePlayerTimedOut = state.status === "running" && state.activePlayer !== null && displayTimes[state.activePlayer] <= 0;
498
+ useEffect(() => {
499
+ if (state.status !== "running" || state.activePlayer === null) {
500
+ return;
501
+ }
502
+ const intervalId = setInterval(() => forceUpdate((c) => c + 1), 100);
503
+ return () => clearInterval(intervalId);
504
+ }, [state.status, state.activePlayer]);
505
+ useEffect(() => {
506
+ var _a, _b;
507
+ if (state.status === "running" && state.activePlayer !== null) {
508
+ (_b = (_a = optionsRef.current).onTimeUpdate) == null ? void 0 : _b.call(_a, displayTimes);
509
+ }
510
+ }, [tick]);
511
+ useEffect(() => {
512
+ var _a, _b;
513
+ if (activePlayerTimedOut && state.activePlayer !== null) {
514
+ dispatch({ type: "TIMEOUT", payload: { player: state.activePlayer } });
515
+ (_b = (_a = optionsRef.current).onTimeout) == null ? void 0 : _b.call(_a, state.activePlayer);
516
+ }
517
+ }, [activePlayerTimedOut, state.activePlayer]);
518
+ const previousActivePlayerRef = useRef(state.activePlayer);
519
+ useEffect(() => {
520
+ var _a, _b;
521
+ if (state.activePlayer !== previousActivePlayerRef.current && state.activePlayer !== null) {
522
+ (_b = (_a = optionsRef.current).onSwitch) == null ? void 0 : _b.call(_a, state.activePlayer);
523
+ }
524
+ previousActivePlayerRef.current = state.activePlayer;
525
+ }, [state.activePlayer]);
526
+ const info = useMemo(
527
+ () => ({
528
+ isRunning: state.status === "running",
529
+ isPaused: state.status === "paused",
530
+ isFinished: state.status === "finished",
531
+ isWhiteActive: state.activePlayer === "white",
532
+ isBlackActive: state.activePlayer === "black",
533
+ hasTimeout: state.timeout !== null,
534
+ // Time odds is based on initial configuration, not current remaining time
535
+ hasTimeOdds: state.initialTimes.white !== state.initialTimes.black
536
+ }),
537
+ [
538
+ state.status,
539
+ state.activePlayer,
540
+ state.timeout,
541
+ state.initialTimes.white,
542
+ state.initialTimes.black
543
+ ]
544
+ );
545
+ const start = useCallback(() => {
546
+ dispatch({ type: "START", payload: { now: Date.now() } });
547
+ }, []);
548
+ const pause = useCallback(() => {
549
+ dispatch({ type: "PAUSE", payload: { now: Date.now() } });
550
+ }, []);
551
+ const resume = useCallback(() => {
552
+ dispatch({ type: "RESUME", payload: { now: Date.now() } });
553
+ }, []);
554
+ const switchPlayer = useCallback(() => {
555
+ const currentState = stateRef.current;
556
+ const now = Date.now();
557
+ let newTimes;
558
+ if (currentState.status !== "delayed" && currentState.activePlayer && currentState.moveStartTime !== null) {
559
+ const timeSpent = now - currentState.moveStartTime;
560
+ const currentTime = currentState.times[currentState.activePlayer];
561
+ const newTime = calculateSwitchTime(
562
+ currentTime,
563
+ timeSpent,
564
+ currentState.config
565
+ );
566
+ newTimes = {
567
+ ...currentState.times,
568
+ [currentState.activePlayer]: newTime
569
+ };
570
+ }
571
+ dispatch({ type: "SWITCH", payload: { newTimes, now } });
572
+ }, []);
573
+ const reset = useCallback((newTimeControl) => {
574
+ const currentOptions = optionsRef.current;
575
+ const resetConfig = newTimeControl ? { ...currentOptions, time: newTimeControl } : currentOptions;
576
+ dispatch({ type: "RESET", payload: { ...resetConfig, now: Date.now() } });
577
+ }, []);
578
+ const addTime = useCallback((player, milliseconds) => {
579
+ dispatch({
580
+ type: "ADD_TIME",
581
+ payload: { player, milliseconds, now: Date.now() }
582
+ });
583
+ }, []);
584
+ const setTime = useCallback((player, milliseconds) => {
585
+ dispatch({
586
+ type: "SET_TIME",
587
+ payload: { player, milliseconds, now: Date.now() }
588
+ });
589
+ }, []);
590
+ const methods = useMemo(
591
+ () => ({
592
+ start,
593
+ pause,
594
+ resume,
595
+ switch: switchPlayer,
596
+ reset,
597
+ addTime,
598
+ setTime
599
+ }),
600
+ [start, pause, resume, switchPlayer, reset, addTime, setTime]
601
+ );
602
+ const periodInfo = useMemo(() => {
603
+ if (!state.periodState) {
604
+ return {
605
+ currentPeriodIndex: { white: 0, black: 0 },
606
+ totalPeriods: 1,
607
+ currentPeriod: {
608
+ white: {
609
+ baseTime: state.config.baseTime / 1e3,
610
+ increment: state.config.increment / 1e3,
611
+ delay: state.config.delay / 1e3
612
+ },
613
+ black: {
614
+ baseTime: state.config.baseTime / 1e3,
615
+ increment: state.config.increment / 1e3,
616
+ delay: state.config.delay / 1e3
617
+ }
618
+ },
619
+ periodMoves: { white: 0, black: 0 }
620
+ };
621
+ }
622
+ const periods = state.periodState.periods;
623
+ const getCurrentPeriod = (periodIndex) => {
624
+ const period = periodIndex >= 0 && periodIndex < periods.length ? periods[periodIndex] : periods[0];
625
+ return {
626
+ ...period,
627
+ baseTime: period.baseTime / 1e3,
628
+ increment: period.increment !== void 0 ? period.increment / 1e3 : void 0,
629
+ delay: period.delay !== void 0 ? period.delay / 1e3 : void 0
630
+ };
631
+ };
632
+ return {
633
+ currentPeriodIndex: state.periodState.periodIndex,
634
+ totalPeriods: periods.length,
635
+ currentPeriod: {
636
+ white: getCurrentPeriod(state.periodState.periodIndex.white),
637
+ black: getCurrentPeriod(state.periodState.periodIndex.black)
638
+ },
639
+ periodMoves: state.periodState.periodMoves
640
+ };
641
+ }, [state.periodState, state.config]);
642
+ return {
643
+ times: displayTimes,
644
+ initialTimes: state.initialTimes,
645
+ status: state.status,
646
+ activePlayer: state.activePlayer,
647
+ timeout: state.timeout,
648
+ timingMethod: state.config.timingMethod,
649
+ info,
650
+ ...periodInfo,
651
+ methods
652
+ };
653
+ }
654
+ function useOptionalChessClock(options) {
655
+ const result = useChessClock(options ?? DISABLED_CLOCK_CONFIG);
656
+ return options === void 0 ? null : result;
657
+ }
658
+
659
+ // src/components/ChessClock/parts/Root.tsx
660
+ var Root = ({
661
+ timeControl,
662
+ children
663
+ }) => {
664
+ const clockState = useChessClock(timeControl);
665
+ return /* @__PURE__ */ React.createElement(ChessClockContext.Provider, { value: clockState }, children);
666
+ };
667
+ Root.displayName = "ChessClock.Root";
668
+
669
+ // src/components/ChessClock/parts/Display.tsx
670
+ import React2 from "react";
671
+ var Display = React2.forwardRef(
672
+ ({
673
+ color,
674
+ format = "auto",
675
+ formatTime: customFormatTime,
676
+ className,
677
+ style,
678
+ ...rest
679
+ }, ref) => {
680
+ const { times, activePlayer, status, timeout } = useChessClockContext();
681
+ const time = times[color];
682
+ const isActive = activePlayer === color;
683
+ const hasTimeout = timeout === color;
684
+ const isPaused = status === "paused";
685
+ const formattedTime = customFormatTime ? customFormatTime(time) : formatClockTime(time, format);
686
+ return /* @__PURE__ */ React2.createElement(
687
+ "div",
688
+ {
689
+ ref,
690
+ className,
691
+ style,
692
+ "data-clock-color": color,
693
+ "data-clock-active": isActive ? "true" : "false",
694
+ "data-clock-paused": isPaused ? "true" : "false",
695
+ "data-clock-timeout": hasTimeout ? "true" : "false",
696
+ "data-clock-status": status,
697
+ ...rest
698
+ },
699
+ formattedTime
700
+ );
701
+ }
702
+ );
703
+ Display.displayName = "ChessClock.Display";
704
+
705
+ // src/components/ChessClock/parts/Switch.tsx
706
+ import React3 from "react";
707
+ import { Slot } from "@radix-ui/react-slot";
708
+ var Switch = React3.forwardRef(
709
+ ({
710
+ asChild = false,
711
+ children,
712
+ onClick,
713
+ disabled,
714
+ className,
715
+ style,
716
+ type,
717
+ ...rest
718
+ }, ref) => {
719
+ const { methods, status } = useChessClockContext();
720
+ const isDisabled = disabled || status === "finished" || status === "idle";
721
+ const handleClick = React3.useCallback(
722
+ (e) => {
723
+ methods.switch();
724
+ onClick == null ? void 0 : onClick(e);
725
+ },
726
+ [methods, onClick]
727
+ );
728
+ return asChild ? /* @__PURE__ */ React3.createElement(
729
+ Slot,
730
+ {
731
+ ref,
732
+ onClick: handleClick,
733
+ className,
734
+ style,
735
+ ...{ ...rest, disabled: isDisabled }
736
+ },
737
+ children
738
+ ) : /* @__PURE__ */ React3.createElement(
739
+ "button",
740
+ {
741
+ ref,
742
+ type: type || "button",
743
+ className,
744
+ style,
745
+ onClick: handleClick,
746
+ disabled: isDisabled,
747
+ ...rest
748
+ },
749
+ children
750
+ );
751
+ }
752
+ );
753
+ Switch.displayName = "ChessClock.Switch";
754
+
755
+ // src/components/ChessClock/parts/PlayPause.tsx
756
+ import React4 from "react";
757
+ import { Slot as Slot2 } from "@radix-ui/react-slot";
758
+ var DEFAULT_CONTENT = {
759
+ start: "Start",
760
+ pause: "Pause",
761
+ resume: "Resume",
762
+ delayed: "Start",
763
+ finished: "Game Over"
764
+ };
765
+ var resolveContent = (isFinished, isDelayed, shouldShowStart, isPaused, isRunning, customContent) => {
766
+ if (isFinished) {
767
+ return customContent.finishedContent ?? DEFAULT_CONTENT.finished;
768
+ }
769
+ if (isDelayed) {
770
+ return customContent.delayedContent ?? DEFAULT_CONTENT.delayed;
771
+ }
772
+ if (shouldShowStart) {
773
+ return customContent.startContent ?? DEFAULT_CONTENT.start;
774
+ }
775
+ if (isPaused) {
776
+ return customContent.resumeContent ?? DEFAULT_CONTENT.resume;
777
+ }
778
+ if (isRunning) {
779
+ return customContent.pauseContent ?? DEFAULT_CONTENT.pause;
780
+ }
781
+ return DEFAULT_CONTENT.start;
782
+ };
783
+ var PlayPause = React4.forwardRef(
784
+ ({
785
+ asChild = false,
786
+ startContent,
787
+ pauseContent,
788
+ resumeContent,
789
+ delayedContent,
790
+ finishedContent,
791
+ children,
792
+ onClick,
793
+ disabled,
794
+ className,
795
+ style,
796
+ type,
797
+ ...rest
798
+ }, ref) => {
799
+ const { status, methods } = useChessClockContext();
800
+ const isIdle = status === "idle";
801
+ const isDelayed = status === "delayed";
802
+ const isPaused = status === "paused";
803
+ const isRunning = status === "running";
804
+ const isFinished = status === "finished";
805
+ const shouldShowStart = isIdle || isDelayed;
806
+ const isDisabled = disabled || isFinished || isDelayed;
807
+ const handleClick = React4.useCallback(
808
+ (e) => {
809
+ if (shouldShowStart) {
810
+ methods.start();
811
+ } else if (isPaused) {
812
+ methods.resume();
813
+ } else if (isRunning) {
814
+ methods.pause();
815
+ }
816
+ onClick == null ? void 0 : onClick(e);
817
+ },
818
+ [shouldShowStart, isPaused, isRunning, methods, onClick]
819
+ );
820
+ const content = children ?? resolveContent(
821
+ isFinished,
822
+ isDelayed,
823
+ shouldShowStart,
824
+ isPaused,
825
+ isRunning,
826
+ {
827
+ startContent,
828
+ pauseContent,
829
+ resumeContent,
830
+ delayedContent,
831
+ finishedContent
832
+ }
833
+ );
834
+ return asChild ? /* @__PURE__ */ React4.createElement(
835
+ Slot2,
836
+ {
837
+ ref,
838
+ onClick: handleClick,
839
+ className,
840
+ style,
841
+ ...{ ...rest, disabled: isDisabled }
842
+ },
843
+ content
844
+ ) : /* @__PURE__ */ React4.createElement(
845
+ "button",
846
+ {
847
+ ref,
848
+ type: type || "button",
849
+ className,
850
+ style,
851
+ onClick: handleClick,
852
+ disabled: isDisabled,
853
+ ...rest
854
+ },
855
+ content
856
+ );
857
+ }
858
+ );
859
+ PlayPause.displayName = "ChessClock.PlayPause";
860
+
861
+ // src/components/ChessClock/parts/Reset.tsx
862
+ import React5 from "react";
863
+ import { Slot as Slot3 } from "@radix-ui/react-slot";
864
+ var Reset = React5.forwardRef(
865
+ ({
866
+ asChild = false,
867
+ timeControl,
868
+ children,
869
+ onClick,
870
+ disabled,
871
+ className,
872
+ style,
873
+ type,
874
+ ...rest
875
+ }, ref) => {
876
+ const { methods, status } = useChessClockContext();
877
+ const isDisabled = disabled || status === "idle";
878
+ const handleClick = React5.useCallback(
879
+ (e) => {
880
+ methods.reset(timeControl);
881
+ onClick == null ? void 0 : onClick(e);
882
+ },
883
+ [methods, timeControl, onClick]
884
+ );
885
+ return asChild ? /* @__PURE__ */ React5.createElement(
886
+ Slot3,
887
+ {
888
+ ref,
889
+ onClick: handleClick,
890
+ className,
891
+ style,
892
+ ...{ ...rest, disabled: isDisabled }
893
+ },
894
+ children
895
+ ) : /* @__PURE__ */ React5.createElement(
896
+ "button",
897
+ {
898
+ ref,
899
+ type: type || "button",
900
+ className,
901
+ style,
902
+ onClick: handleClick,
903
+ disabled: isDisabled,
904
+ ...rest
905
+ },
906
+ children
907
+ );
908
+ }
909
+ );
910
+ Reset.displayName = "ChessClock.Reset";
911
+
912
+ // src/components/ChessClock/index.ts
913
+ var ChessClock = {
914
+ Root,
915
+ Display,
916
+ Switch,
917
+ PlayPause,
918
+ Reset
919
+ };
920
+
921
+ // src/utils/presets.ts
922
+ var presets = {
923
+ // Bullet (< 3 minutes)
924
+ bullet1_0: { baseTime: 60, increment: 0 },
925
+ bullet1_1: { baseTime: 60, increment: 1 },
926
+ bullet2_1: { baseTime: 120, increment: 1 },
927
+ // Blitz (3-10 minutes)
928
+ blitz3_0: { baseTime: 180, increment: 0 },
929
+ blitz3_2: { baseTime: 180, increment: 2 },
930
+ blitz5_0: { baseTime: 300, increment: 0 },
931
+ blitz5_3: { baseTime: 300, increment: 3 },
932
+ // Rapid (10-60 minutes)
933
+ rapid10_0: { baseTime: 600, increment: 0 },
934
+ rapid10_5: { baseTime: 600, increment: 5 },
935
+ rapid15_10: { baseTime: 900, increment: 10 },
936
+ // Classical (≥ 60 minutes)
937
+ classical30_0: { baseTime: 1800, increment: 0 },
938
+ classical90_30: { baseTime: 5400, increment: 30 },
939
+ // Tournament (multi-period)
940
+ fideClassical: [
941
+ { baseTime: 5400, increment: 30, moves: 40 },
942
+ { baseTime: 1800, increment: 30, moves: 20 },
943
+ { baseTime: 900, increment: 30 }
944
+ ],
945
+ uscfClassical: [
946
+ { baseTime: 7200, moves: 40 },
947
+ { baseTime: 3600, moves: 20 },
948
+ { baseTime: 1800 }
949
+ ]
950
+ };
951
+ export {
952
+ ChessClock,
953
+ ChessClockContext,
954
+ Display,
955
+ PlayPause,
956
+ Reset,
957
+ Switch,
958
+ formatClockTime,
959
+ getInitialTimes,
960
+ normalizeTimeControl,
961
+ parseMultiPeriodTimeControl,
962
+ parseTimeControlConfig,
963
+ parseTimeControlString,
964
+ presets,
965
+ useChessClock,
966
+ useChessClockContext,
967
+ useOptionalChessClock
968
+ };
969
+ //# sourceMappingURL=index.js.map