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