@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
@@ -0,0 +1,782 @@
1
+ import type { Meta } from "@storybook/react";
2
+ import React from "react";
3
+ import { ChessClock } from "./index";
4
+ import { useChessClockContext } from "../../hooks/useChessClockContext";
5
+
6
+ // ============================================================================
7
+ // Design Tokens
8
+ // ============================================================================
9
+ const color = {
10
+ bg: "#f5f5f0",
11
+ surface: "#ffffff",
12
+ border: "#e2e0db",
13
+ text: "#2d2d2d",
14
+ textSecondary: "#7a7a72",
15
+ textMuted: "#a5a59c",
16
+ accent: "#5b8a3c",
17
+ dark: "#1c1c1a",
18
+ info: "#e7f5ff",
19
+ infoBorder: "#b4d5f0",
20
+ infoText: "#1864ab",
21
+ warn: "#b58a1b",
22
+ warnBg: "#fff9db",
23
+ };
24
+
25
+ const font = {
26
+ sans: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
27
+ mono: "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace",
28
+ };
29
+
30
+ // ============================================================================
31
+ // Shared Styles
32
+ // ============================================================================
33
+ const s = {
34
+ container: {
35
+ display: "flex",
36
+ flexDirection: "column" as const,
37
+ alignItems: "center",
38
+ gap: "16px",
39
+ padding: "24px",
40
+ fontFamily: font.sans,
41
+ maxWidth: "420px",
42
+ margin: "0 auto",
43
+ },
44
+ header: {
45
+ textAlign: "center" as const,
46
+ },
47
+ title: {
48
+ fontSize: "15px",
49
+ fontWeight: 600,
50
+ color: color.text,
51
+ margin: "0 0 4px",
52
+ letterSpacing: "-0.01em",
53
+ },
54
+ subtitle: {
55
+ fontSize: "13px",
56
+ color: color.textSecondary,
57
+ margin: 0,
58
+ lineHeight: 1.4,
59
+ },
60
+ controls: {
61
+ display: "flex",
62
+ gap: "8px",
63
+ justifyContent: "center",
64
+ flexWrap: "wrap" as const,
65
+ },
66
+ btn: {
67
+ padding: "7px 14px",
68
+ fontSize: "13px",
69
+ fontWeight: 500,
70
+ fontFamily: font.sans,
71
+ cursor: "pointer",
72
+ border: `1px solid ${color.border}`,
73
+ borderRadius: "4px",
74
+ backgroundColor: color.surface,
75
+ color: color.text,
76
+ } as React.CSSProperties,
77
+ btnPrimary: {
78
+ backgroundColor: color.accent,
79
+ borderColor: color.accent,
80
+ color: "#fff",
81
+ } as React.CSSProperties,
82
+ btnDisabled: {
83
+ opacity: 0.5,
84
+ cursor: "not-allowed",
85
+ } as React.CSSProperties,
86
+ info: {
87
+ padding: "10px 14px",
88
+ backgroundColor: color.info,
89
+ border: `1px solid ${color.infoBorder}`,
90
+ borderRadius: "5px",
91
+ fontSize: "12px",
92
+ color: color.infoText,
93
+ textAlign: "center" as const,
94
+ lineHeight: 1.5,
95
+ },
96
+ logBox: {
97
+ width: "100%",
98
+ padding: "10px 12px",
99
+ backgroundColor: color.bg,
100
+ border: `1px solid ${color.border}`,
101
+ borderRadius: "5px",
102
+ fontSize: "11px",
103
+ fontFamily: font.mono,
104
+ maxHeight: "90px",
105
+ overflow: "auto",
106
+ color: color.textSecondary,
107
+ },
108
+ radioGroup: {
109
+ display: "flex",
110
+ gap: "8px",
111
+ flexWrap: "wrap" as const,
112
+ justifyContent: "center",
113
+ },
114
+ radioLabel: {
115
+ display: "flex",
116
+ alignItems: "center",
117
+ gap: "4px",
118
+ fontSize: "13px",
119
+ fontWeight: 500,
120
+ color: color.text,
121
+ cursor: "pointer",
122
+ },
123
+ };
124
+
125
+ const clock = {
126
+ row: {
127
+ display: "flex",
128
+ gap: "12px",
129
+ justifyContent: "center",
130
+ alignItems: "center",
131
+ },
132
+ cell: {
133
+ display: "flex",
134
+ flexDirection: "column" as const,
135
+ alignItems: "center",
136
+ gap: "4px",
137
+ },
138
+ label: {
139
+ fontSize: "11px",
140
+ fontWeight: 600,
141
+ color: color.textMuted,
142
+ textTransform: "uppercase" as const,
143
+ letterSpacing: "0.06em",
144
+ },
145
+ white: {
146
+ padding: "10px 20px",
147
+ fontSize: "24px",
148
+ fontWeight: 600,
149
+ fontFamily: font.mono,
150
+ borderRadius: "5px",
151
+ textAlign: "center" as const,
152
+ minWidth: "100px",
153
+ backgroundColor: color.surface,
154
+ border: `2px solid ${color.text}`,
155
+ color: color.text,
156
+ },
157
+ black: {
158
+ padding: "10px 20px",
159
+ fontSize: "24px",
160
+ fontWeight: 600,
161
+ fontFamily: font.mono,
162
+ borderRadius: "5px",
163
+ textAlign: "center" as const,
164
+ minWidth: "100px",
165
+ backgroundColor: color.dark,
166
+ border: `2px solid ${color.dark}`,
167
+ color: "#f0f0ec",
168
+ },
169
+ };
170
+
171
+ // ============================================================================
172
+ // Reusable Helpers
173
+ // ============================================================================
174
+
175
+ const ClockPair = ({
176
+ format,
177
+ }: {
178
+ format?: "auto" | "mm:ss" | "ss.d" | "hh:mm:ss";
179
+ }) => (
180
+ <div style={clock.row}>
181
+ <div style={clock.cell}>
182
+ <span style={clock.label}>White</span>
183
+ <ChessClock.Display color="white" format={format} style={clock.white} />
184
+ </div>
185
+ <div style={clock.cell}>
186
+ <span style={clock.label}>Black</span>
187
+ <ChessClock.Display color="black" format={format} style={clock.black} />
188
+ </div>
189
+ </div>
190
+ );
191
+
192
+ const PlayPauseBtn = () => {
193
+ const { status } = useChessClockContext();
194
+ const isDisabled = status === "finished" || status === "delayed";
195
+
196
+ return (
197
+ <ChessClock.PlayPause
198
+ style={{
199
+ ...s.btn,
200
+ ...s.btnPrimary,
201
+ ...(isDisabled ? s.btnDisabled : {}),
202
+ }}
203
+ startContent="Start"
204
+ pauseContent="Pause"
205
+ resumeContent="Resume"
206
+ finishedContent="Game Over"
207
+ />
208
+ );
209
+ };
210
+
211
+ const SwitchBtn = () => (
212
+ <ChessClock.Switch style={s.btn}>Switch</ChessClock.Switch>
213
+ );
214
+
215
+ const ResetBtn = () => <ChessClock.Reset style={s.btn}>Reset</ChessClock.Reset>;
216
+
217
+ const Controls = ({ children }: { children?: React.ReactNode }) => (
218
+ <div style={s.controls}>
219
+ <PlayPauseBtn />
220
+ <SwitchBtn />
221
+ {children}
222
+ </div>
223
+ );
224
+
225
+ // ============================================================================
226
+ // Meta
227
+ // ============================================================================
228
+ const meta = {
229
+ title: "react-chess-clock/Components/ChessClock",
230
+ component: ChessClock.Root,
231
+ tags: ["components", "clock", "timer"],
232
+ argTypes: {},
233
+ parameters: {
234
+ actions: { argTypesRegex: "^_on.*" },
235
+ layout: "centered",
236
+ },
237
+ } satisfies Meta<typeof ChessClock.Root>;
238
+
239
+ export default meta;
240
+
241
+ // ============================================================================
242
+ // 1. Default
243
+ // ============================================================================
244
+
245
+ export const Default = () => (
246
+ <ChessClock.Root timeControl={{ time: "5+3" }}>
247
+ <div style={s.container}>
248
+ <div style={s.header}>
249
+ <h3 style={s.title}>Blitz &middot; 5+3</h3>
250
+ <p style={s.subtitle}>
251
+ 5 minutes with 3-second Fischer increment (delayed start)
252
+ </p>
253
+ </div>
254
+ <ClockPair />
255
+ <div style={s.info}>Clock starts after Black&apos;s first move</div>
256
+ <Controls>
257
+ <ResetBtn />
258
+ </Controls>
259
+ </div>
260
+ </ChessClock.Root>
261
+ );
262
+
263
+ // ============================================================================
264
+ // 2. TimingMethods
265
+ // ============================================================================
266
+
267
+ export const TimingMethods = () => {
268
+ const [method, setMethod] = React.useState<"fischer" | "delay" | "bronstein">(
269
+ "fischer",
270
+ );
271
+
272
+ const descriptions: Record<string, string> = {
273
+ fischer: "Adds increment to your clock after each move.",
274
+ delay: "Countdown waits for the delay period before decrementing.",
275
+ bronstein:
276
+ "Adds back actual time used (up to delay amount) after each move.",
277
+ };
278
+
279
+ return (
280
+ <ChessClock.Root
281
+ key={method}
282
+ timeControl={{
283
+ time: {
284
+ baseTime: 300,
285
+ increment: method === "fischer" ? 3 : 0,
286
+ delay: method !== "fischer" ? 3 : 0,
287
+ },
288
+ timingMethod: method,
289
+ }}
290
+ >
291
+ <div style={s.container}>
292
+ <div style={s.header}>
293
+ <h3 style={s.title}>Timing Methods</h3>
294
+ <p style={s.subtitle}>Compare Fischer, Delay, and Bronstein</p>
295
+ </div>
296
+ <div style={s.radioGroup}>
297
+ {(["fischer", "delay", "bronstein"] as const).map((m) => (
298
+ <label key={m} style={s.radioLabel}>
299
+ <input
300
+ type="radio"
301
+ name="timing"
302
+ checked={method === m}
303
+ onChange={() => setMethod(m)}
304
+ />
305
+ {m.charAt(0).toUpperCase() + m.slice(1)}
306
+ </label>
307
+ ))}
308
+ </div>
309
+ <ClockPair />
310
+ <div style={s.info}>{descriptions[method]}</div>
311
+ <Controls>
312
+ <ResetBtn />
313
+ </Controls>
314
+ </div>
315
+ </ChessClock.Root>
316
+ );
317
+ };
318
+
319
+ // ============================================================================
320
+ // 3. ClockStartModes
321
+ // ============================================================================
322
+
323
+ export const ClockStartModes = () => {
324
+ const [mode, setMode] = React.useState<"delayed" | "immediate" | "manual">(
325
+ "delayed",
326
+ );
327
+
328
+ const descriptions: Record<string, string> = {
329
+ delayed:
330
+ "Clock starts after Black's first move (Lichess-style). White moves → Black moves → Clock starts.",
331
+ immediate:
332
+ "White's clock starts counting down immediately on first switch (Chess.com-style).",
333
+ manual: "Clock stays idle until you press Start.",
334
+ };
335
+
336
+ return (
337
+ <ChessClock.Root key={mode} timeControl={{ time: "5+3", clockStart: mode }}>
338
+ <div style={s.container}>
339
+ <div style={s.header}>
340
+ <h3 style={s.title}>Clock Start Modes</h3>
341
+ <p style={s.subtitle}>Controls when the clock begins counting down</p>
342
+ </div>
343
+ <div style={s.radioGroup}>
344
+ {(["delayed", "immediate", "manual"] as const).map((m) => (
345
+ <label key={m} style={s.radioLabel}>
346
+ <input
347
+ type="radio"
348
+ name="clockStart"
349
+ checked={mode === m}
350
+ onChange={() => setMode(m)}
351
+ />
352
+ {m.charAt(0).toUpperCase() + m.slice(1)}
353
+ </label>
354
+ ))}
355
+ </div>
356
+ <ClockPair />
357
+ <div style={s.info}>{descriptions[mode]}</div>
358
+ <Controls>
359
+ <ResetBtn />
360
+ </Controls>
361
+ </div>
362
+ </ChessClock.Root>
363
+ );
364
+ };
365
+
366
+ // ============================================================================
367
+ // 4. TimeOdds
368
+ // ============================================================================
369
+
370
+ export const TimeOdds = () => (
371
+ <ChessClock.Root timeControl={{ time: "5", whiteTime: 300, blackTime: 180 }}>
372
+ <div style={s.container}>
373
+ <div style={s.header}>
374
+ <h3 style={s.title}>Time Odds</h3>
375
+ <p style={s.subtitle}>White 5 min vs Black 3 min</p>
376
+ </div>
377
+ <ClockPair />
378
+ <Controls>
379
+ <ResetBtn />
380
+ </Controls>
381
+ </div>
382
+ </ChessClock.Root>
383
+ );
384
+
385
+ // ============================================================================
386
+ // 5. DisplayFormats
387
+ // ============================================================================
388
+
389
+ const fmt = {
390
+ row: {
391
+ display: "flex",
392
+ justifyContent: "space-between",
393
+ alignItems: "center",
394
+ padding: "6px 10px",
395
+ backgroundColor: color.bg,
396
+ borderRadius: "4px",
397
+ },
398
+ label: {
399
+ fontSize: "12px",
400
+ fontWeight: 500,
401
+ color: color.textSecondary,
402
+ fontFamily: font.mono,
403
+ },
404
+ display: {
405
+ padding: "6px 14px",
406
+ fontSize: "16px",
407
+ fontWeight: 600,
408
+ fontFamily: font.mono,
409
+ borderRadius: "4px",
410
+ backgroundColor: color.surface,
411
+ border: `2px solid ${color.text}`,
412
+ color: color.text,
413
+ },
414
+ };
415
+
416
+ export const DisplayFormats = () => (
417
+ <ChessClock.Root
418
+ timeControl={{
419
+ time: { baseTime: 20, increment: 0 },
420
+ clockStart: "immediate",
421
+ }}
422
+ >
423
+ <div style={s.container}>
424
+ <div style={s.header}>
425
+ <h3 style={s.title}>Display Formats</h3>
426
+ <p style={s.subtitle}>
427
+ All built-in formats + custom formatTime (20s base to show auto
428
+ decimals)
429
+ </p>
430
+ </div>
431
+ <div
432
+ style={{
433
+ display: "flex",
434
+ flexDirection: "column",
435
+ gap: "6px",
436
+ width: "100%",
437
+ }}
438
+ >
439
+ <div style={fmt.row}>
440
+ <span style={fmt.label}>format=&quot;auto&quot;</span>
441
+ <ChessClock.Display color="white" format="auto" style={fmt.display} />
442
+ </div>
443
+ <div style={fmt.row}>
444
+ <span style={fmt.label}>format=&quot;mm:ss&quot;</span>
445
+ <ChessClock.Display
446
+ color="white"
447
+ format="mm:ss"
448
+ style={fmt.display}
449
+ />
450
+ </div>
451
+ <div style={fmt.row}>
452
+ <span style={fmt.label}>format=&quot;hh:mm:ss&quot;</span>
453
+ <ChessClock.Display
454
+ color="white"
455
+ format="hh:mm:ss"
456
+ style={fmt.display}
457
+ />
458
+ </div>
459
+ <div style={fmt.row}>
460
+ <span style={fmt.label}>format=&quot;ss.d&quot;</span>
461
+ <ChessClock.Display color="white" format="ss.d" style={fmt.display} />
462
+ </div>
463
+ <div style={fmt.row}>
464
+ <span style={fmt.label}>Custom fn</span>
465
+ <ChessClock.Display
466
+ color="white"
467
+ formatTime={(ms) => `${Math.ceil(ms / 1000)}s`}
468
+ style={fmt.display}
469
+ />
470
+ </div>
471
+ </div>
472
+ <Controls>
473
+ <ResetBtn />
474
+ </Controls>
475
+ </div>
476
+ </ChessClock.Root>
477
+ );
478
+
479
+ // ============================================================================
480
+ // 6. Callbacks
481
+ // ============================================================================
482
+
483
+ export const Callbacks = () => {
484
+ const [logs, setLogs] = React.useState<string[]>([]);
485
+ const addLog = (msg: string) => setLogs((prev) => [...prev.slice(-4), msg]);
486
+
487
+ return (
488
+ <ChessClock.Root
489
+ timeControl={{
490
+ time: "1+5",
491
+ clockStart: "immediate",
492
+ onTimeout: (loser) => addLog(`Timeout! ${loser} loses on time`),
493
+ onSwitch: (active) => addLog(`Switch: ${active} is now active`),
494
+ onTimeUpdate: (times) =>
495
+ addLog(`Time: W=${times.white}ms B=${times.black}ms`),
496
+ }}
497
+ >
498
+ <div style={s.container}>
499
+ <div style={s.header}>
500
+ <h3 style={s.title}>Callbacks &middot; 1+5</h3>
501
+ <p style={s.subtitle}>
502
+ onTimeout, onSwitch, onTimeUpdate event logging
503
+ </p>
504
+ </div>
505
+ <ClockPair format="ss.d" />
506
+ <Controls>
507
+ <ResetBtn />
508
+ </Controls>
509
+ <div style={s.logBox}>
510
+ {logs.length === 0 ? (
511
+ <span style={{ color: color.textMuted, fontStyle: "italic" }}>
512
+ Events will appear here...
513
+ </span>
514
+ ) : (
515
+ logs.map((log, i) => <div key={i}>{log}</div>)
516
+ )}
517
+ </div>
518
+ </div>
519
+ </ChessClock.Root>
520
+ );
521
+ };
522
+
523
+ // ============================================================================
524
+ // 7. DynamicReset
525
+ // ============================================================================
526
+
527
+ export const DynamicReset = () => (
528
+ <ChessClock.Root timeControl={{ time: "5+3" }}>
529
+ <div style={s.container}>
530
+ <div style={s.header}>
531
+ <h3 style={s.title}>Dynamic Reset</h3>
532
+ <p style={s.subtitle}>
533
+ Reset with a different time control using the timeControl prop
534
+ </p>
535
+ </div>
536
+ <ClockPair />
537
+ <Controls />
538
+ <div style={s.controls}>
539
+ <ChessClock.Reset style={s.btn}>Reset</ChessClock.Reset>
540
+ <ChessClock.Reset style={s.btn} timeControl="1">
541
+ 1+0
542
+ </ChessClock.Reset>
543
+ <ChessClock.Reset style={s.btn} timeControl="3+2">
544
+ 3+2
545
+ </ChessClock.Reset>
546
+ <ChessClock.Reset style={s.btn} timeControl="5+3">
547
+ 5+3
548
+ </ChessClock.Reset>
549
+ <ChessClock.Reset style={s.btn} timeControl="10">
550
+ 10+0
551
+ </ChessClock.Reset>
552
+ </div>
553
+ </div>
554
+ </ChessClock.Root>
555
+ );
556
+
557
+ // ============================================================================
558
+ // 8. MultiPeriod
559
+ // ============================================================================
560
+
561
+ function PeriodInfo() {
562
+ const { currentPeriodIndex, totalPeriods, periodMoves, currentPeriod } =
563
+ useChessClockContext();
564
+
565
+ const periodLabel = (playerColor: "white" | "black") => {
566
+ const idx = currentPeriodIndex[playerColor];
567
+ const moves = periodMoves[playerColor];
568
+ const period = currentPeriod[playerColor];
569
+ const required = period.moves;
570
+ return required
571
+ ? `Period ${idx + 1}/${totalPeriods} — ${moves}/${required} moves`
572
+ : `Period ${idx + 1}/${totalPeriods} — Sudden death`;
573
+ };
574
+
575
+ return (
576
+ <div
577
+ style={{
578
+ width: "100%",
579
+ display: "flex",
580
+ flexDirection: "column",
581
+ gap: "4px",
582
+ }}
583
+ >
584
+ {(["white", "black"] as const).map((c) => (
585
+ <div
586
+ key={c}
587
+ style={{
588
+ display: "flex",
589
+ justifyContent: "space-between",
590
+ padding: "6px 10px",
591
+ backgroundColor: c === "white" ? color.bg : color.dark,
592
+ borderRadius: "4px",
593
+ fontSize: "12px",
594
+ fontFamily: font.mono,
595
+ color: c === "white" ? color.text : "#f0f0ec",
596
+ }}
597
+ >
598
+ <span style={{ textTransform: "capitalize" }}>{c}</span>
599
+ <span>{periodLabel(c)}</span>
600
+ </div>
601
+ ))}
602
+ </div>
603
+ );
604
+ }
605
+
606
+ export const MultiPeriod = () => (
607
+ <ChessClock.Root
608
+ timeControl={{
609
+ time: [
610
+ { baseTime: 5400, increment: 30, moves: 40 },
611
+ { baseTime: 1800, increment: 30, moves: 20 },
612
+ { baseTime: 900, increment: 30 },
613
+ ],
614
+ clockStart: "manual",
615
+ }}
616
+ >
617
+ <div style={s.container}>
618
+ <div style={s.header}>
619
+ <h3 style={s.title}>FIDE Classical</h3>
620
+ <p style={s.subtitle}>90min/40 + 30min/20 + 15min SD</p>
621
+ </div>
622
+ <ClockPair />
623
+ <PeriodInfo />
624
+ <div style={s.info}>
625
+ Each player advances independently through 3 periods
626
+ </div>
627
+ <Controls>
628
+ <ResetBtn />
629
+ </Controls>
630
+ </div>
631
+ </ChessClock.Root>
632
+ );
633
+
634
+ // ============================================================================
635
+ // 9. ServerSync
636
+ // ============================================================================
637
+
638
+ function ServerTimeSyncHelper({
639
+ serverTimes,
640
+ }: {
641
+ serverTimes: { white: number; black: number };
642
+ }) {
643
+ const { methods } = useChessClockContext();
644
+ const prevTimes = React.useRef(serverTimes);
645
+
646
+ React.useEffect(() => {
647
+ if (serverTimes.white !== prevTimes.current.white) {
648
+ methods.setTime("white", serverTimes.white);
649
+ }
650
+ if (serverTimes.black !== prevTimes.current.black) {
651
+ methods.setTime("black", serverTimes.black);
652
+ }
653
+ prevTimes.current = serverTimes;
654
+ }, [serverTimes, methods]);
655
+
656
+ return null;
657
+ }
658
+
659
+ export const ServerSync = () => {
660
+ const [serverTimes, setServerTimes] = React.useState({
661
+ white: 300000,
662
+ black: 300000,
663
+ });
664
+
665
+ React.useEffect(() => {
666
+ const interval = setInterval(() => {
667
+ setServerTimes((prev) => ({
668
+ white: Math.max(0, prev.white - 1000),
669
+ black: Math.max(0, prev.black - 2000),
670
+ }));
671
+ }, 500);
672
+ return () => clearInterval(interval);
673
+ }, []);
674
+
675
+ return (
676
+ <ChessClock.Root
677
+ timeControl={{
678
+ time: "5+0",
679
+ onTimeout: (loser) => console.log(`${loser} timed out`),
680
+ }}
681
+ >
682
+ <ServerTimeSyncHelper serverTimes={serverTimes} />
683
+ <div style={s.container}>
684
+ <div style={s.header}>
685
+ <h3 style={s.title}>Server Sync</h3>
686
+ <p style={s.subtitle}>Times synced from server via setTime()</p>
687
+ </div>
688
+ <ClockPair format="mm:ss" />
689
+ <Controls>
690
+ <ResetBtn />
691
+ </Controls>
692
+ <div
693
+ style={{
694
+ ...s.info,
695
+ backgroundColor: color.warnBg,
696
+ borderColor: "#e0d3a8",
697
+ color: color.warn,
698
+ }}
699
+ >
700
+ <span style={{ fontFamily: font.mono, fontSize: "11px" }}>
701
+ Server: W={Math.ceil(serverTimes.white / 1000)}s B=
702
+ {Math.ceil(serverTimes.black / 1000)}s
703
+ </span>
704
+ </div>
705
+ </div>
706
+ </ChessClock.Root>
707
+ );
708
+ };
709
+
710
+ // ============================================================================
711
+ // 10. AsChild
712
+ // ============================================================================
713
+
714
+ function AsChildStatus() {
715
+ const { status } = useChessClockContext();
716
+ return (
717
+ <div
718
+ style={{
719
+ fontSize: "12px",
720
+ fontFamily: font.mono,
721
+ color: color.textSecondary,
722
+ }}
723
+ >
724
+ Status: {status}
725
+ </div>
726
+ );
727
+ }
728
+
729
+ export const AsChild = () => {
730
+ const customBtn: React.CSSProperties = {
731
+ ...s.btn,
732
+ display: "inline-flex",
733
+ alignItems: "center",
734
+ gap: "6px",
735
+ userSelect: "none",
736
+ };
737
+
738
+ return (
739
+ <ChessClock.Root timeControl={{ time: "5+3", clockStart: "manual" }}>
740
+ <div style={s.container}>
741
+ <div style={s.header}>
742
+ <h3 style={s.title}>asChild Pattern</h3>
743
+ <p style={s.subtitle}>
744
+ All controls rendered as custom elements via asChild
745
+ </p>
746
+ </div>
747
+ <ClockPair />
748
+ <div style={s.controls}>
749
+ <ChessClock.PlayPause
750
+ asChild
751
+ startContent="Start"
752
+ pauseContent="Pause"
753
+ resumeContent="Resume"
754
+ finishedContent="Game Over"
755
+ >
756
+ <div style={{ ...customBtn, ...s.btnPrimary }}>
757
+ <span>placeholder</span>
758
+ </div>
759
+ </ChessClock.PlayPause>
760
+
761
+ <ChessClock.Switch asChild>
762
+ <div style={customBtn}>
763
+ <span>Switch</span>
764
+ </div>
765
+ </ChessClock.Switch>
766
+
767
+ <ChessClock.Reset asChild>
768
+ <div style={customBtn}>
769
+ <span>Reset</span>
770
+ </div>
771
+ </ChessClock.Reset>
772
+ </div>
773
+ <AsChildStatus />
774
+ <div style={s.info}>
775
+ All 3 buttons use asChild with custom &lt;div&gt; elements.
776
+ <br />
777
+ Disabled state propagates automatically (try when idle or finished).
778
+ </div>
779
+ </div>
780
+ </ChessClock.Root>
781
+ );
782
+ };