react-native-chess-kit 0.1.0 → 0.2.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 (109) hide show
  1. package/lib/commonjs/board-annotations.js +131 -0
  2. package/lib/commonjs/board-annotations.js.map +1 -0
  3. package/lib/commonjs/board-arrows.js +164 -0
  4. package/lib/commonjs/board-arrows.js.map +1 -0
  5. package/lib/commonjs/board-highlights.js +212 -0
  6. package/lib/commonjs/board-highlights.js.map +1 -0
  7. package/lib/commonjs/board-piece.js +71 -25
  8. package/lib/commonjs/board-piece.js.map +1 -1
  9. package/lib/commonjs/board-pieces.js +2 -0
  10. package/lib/commonjs/board-pieces.js.map +1 -1
  11. package/lib/commonjs/board.js +392 -42
  12. package/lib/commonjs/board.js.map +1 -1
  13. package/lib/commonjs/constants.js +104 -0
  14. package/lib/commonjs/constants.js.map +1 -0
  15. package/lib/commonjs/index.js +128 -0
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/commonjs/pieces/default-pieces.js +536 -0
  18. package/lib/commonjs/pieces/default-pieces.js.map +1 -0
  19. package/lib/commonjs/pieces/index.js +13 -0
  20. package/lib/commonjs/pieces/index.js.map +1 -0
  21. package/lib/commonjs/promotion-picker.js +129 -0
  22. package/lib/commonjs/promotion-picker.js.map +1 -0
  23. package/lib/commonjs/static-board.js +150 -0
  24. package/lib/commonjs/static-board.js.map +1 -0
  25. package/lib/commonjs/themes.js +175 -0
  26. package/lib/commonjs/themes.js.map +1 -0
  27. package/lib/commonjs/use-board-gesture.js +184 -11
  28. package/lib/commonjs/use-board-gesture.js.map +1 -1
  29. package/lib/commonjs/use-premove.js +44 -0
  30. package/lib/commonjs/use-premove.js.map +1 -0
  31. package/lib/module/board-annotations.js +126 -0
  32. package/lib/module/board-annotations.js.map +1 -0
  33. package/lib/module/board-arrows.js +161 -0
  34. package/lib/module/board-arrows.js.map +1 -0
  35. package/lib/module/board-highlights.js +206 -0
  36. package/lib/module/board-highlights.js.map +1 -0
  37. package/lib/module/board-piece.js +72 -26
  38. package/lib/module/board-piece.js.map +1 -1
  39. package/lib/module/board-pieces.js +2 -0
  40. package/lib/module/board-pieces.js.map +1 -1
  41. package/lib/module/board.js +395 -44
  42. package/lib/module/board.js.map +1 -1
  43. package/lib/module/constants.js +100 -0
  44. package/lib/module/constants.js.map +1 -0
  45. package/lib/module/index.js +29 -1
  46. package/lib/module/index.js.map +1 -1
  47. package/lib/module/pieces/default-pieces.js +530 -0
  48. package/lib/module/pieces/default-pieces.js.map +1 -0
  49. package/lib/module/pieces/index.js +4 -0
  50. package/lib/module/pieces/index.js.map +1 -0
  51. package/lib/module/promotion-picker.js +124 -0
  52. package/lib/module/promotion-picker.js.map +1 -0
  53. package/lib/module/static-board.js +146 -0
  54. package/lib/module/static-board.js.map +1 -0
  55. package/lib/module/themes.js +171 -0
  56. package/lib/module/themes.js.map +1 -0
  57. package/lib/module/use-board-gesture.js +185 -11
  58. package/lib/module/use-board-gesture.js.map +1 -1
  59. package/lib/module/use-premove.js +40 -0
  60. package/lib/module/use-premove.js.map +1 -0
  61. package/lib/typescript/board-annotations.d.ts +30 -0
  62. package/lib/typescript/board-annotations.d.ts.map +1 -0
  63. package/lib/typescript/board-arrows.d.ts +27 -0
  64. package/lib/typescript/board-arrows.d.ts.map +1 -0
  65. package/lib/typescript/board-highlights.d.ts +65 -0
  66. package/lib/typescript/board-highlights.d.ts.map +1 -0
  67. package/lib/typescript/board-piece.d.ts +19 -9
  68. package/lib/typescript/board-piece.d.ts.map +1 -1
  69. package/lib/typescript/board-pieces.d.ts +2 -1
  70. package/lib/typescript/board-pieces.d.ts.map +1 -1
  71. package/lib/typescript/board.d.ts +11 -2
  72. package/lib/typescript/board.d.ts.map +1 -1
  73. package/lib/typescript/constants.d.ts +54 -0
  74. package/lib/typescript/constants.d.ts.map +1 -0
  75. package/lib/typescript/index.d.ts +9 -1
  76. package/lib/typescript/index.d.ts.map +1 -1
  77. package/lib/typescript/pieces/default-pieces.d.ts +3 -0
  78. package/lib/typescript/pieces/default-pieces.d.ts.map +1 -0
  79. package/lib/typescript/pieces/index.d.ts +2 -0
  80. package/lib/typescript/pieces/index.d.ts.map +1 -0
  81. package/lib/typescript/promotion-picker.d.ts +30 -0
  82. package/lib/typescript/promotion-picker.d.ts.map +1 -0
  83. package/lib/typescript/static-board.d.ts +12 -0
  84. package/lib/typescript/static-board.d.ts.map +1 -0
  85. package/lib/typescript/themes.d.ts +15 -0
  86. package/lib/typescript/themes.d.ts.map +1 -0
  87. package/lib/typescript/types.d.ts +194 -24
  88. package/lib/typescript/types.d.ts.map +1 -1
  89. package/lib/typescript/use-board-gesture.d.ts +28 -2
  90. package/lib/typescript/use-board-gesture.d.ts.map +1 -1
  91. package/lib/typescript/use-premove.d.ts +31 -0
  92. package/lib/typescript/use-premove.d.ts.map +1 -0
  93. package/package.json +4 -2
  94. package/src/board-annotations.tsx +147 -0
  95. package/src/board-arrows.tsx +197 -0
  96. package/src/board-highlights.tsx +226 -0
  97. package/src/board-piece.tsx +77 -29
  98. package/src/board-pieces.tsx +4 -1
  99. package/src/board.tsx +462 -46
  100. package/src/constants.ts +100 -0
  101. package/src/index.ts +62 -1
  102. package/src/pieces/default-pieces.tsx +383 -0
  103. package/src/pieces/index.ts +1 -0
  104. package/src/promotion-picker.tsx +147 -0
  105. package/src/static-board.tsx +150 -0
  106. package/src/themes.ts +129 -0
  107. package/src/types.ts +251 -25
  108. package/src/use-board-gesture.ts +219 -8
  109. package/src/use-premove.ts +59 -0
@@ -5,15 +5,47 @@ import {
5
5
  runOnJS,
6
6
  } from 'react-native-reanimated';
7
7
 
8
- import type { ChessColor, MoveMethod, BoardPiece, LegalMoveTarget, GestureState } from './types';
8
+ import type {
9
+ ChessColor,
10
+ MoveMethod,
11
+ BoardPiece,
12
+ LegalMoveTarget,
13
+ GestureState,
14
+ PieceCode,
15
+ HapticType,
16
+ PremoveData,
17
+ } from './types';
9
18
  import { xyToSquare } from './use-board-pieces';
10
19
 
20
+ // ---------------------------------------------------------------------------
21
+ // Callback types
22
+ // ---------------------------------------------------------------------------
23
+
11
24
  type GestureCallbacks = {
12
25
  onPieceSelected: (square: string) => void;
13
26
  onPieceMoved: (from: string, to: string) => void;
14
27
  onSelectionCleared: () => void;
15
28
  };
16
29
 
30
+ /** Rich callbacks exposed to consumers (all optional) */
31
+ type RichCallbacks = {
32
+ onPieceClick?: (square: string, piece: PieceCode) => void;
33
+ onSquareClick?: (square: string) => void;
34
+ onPieceDragBegin?: (square: string, piece: PieceCode) => void;
35
+ onPieceDragEnd?: (square: string, piece: PieceCode) => void;
36
+ onSquareLongPress?: (square: string) => void;
37
+ onHaptic?: (type: HapticType) => void;
38
+ };
39
+
40
+ /** Premove-related callbacks from board.tsx */
41
+ type PremoveCallbacks = {
42
+ onPremoveSet?: (premove: PremoveData) => void;
43
+ };
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Params
47
+ // ---------------------------------------------------------------------------
48
+
17
49
  type UseBoardGestureParams = {
18
50
  squareSize: number;
19
51
  orientation: ChessColor;
@@ -22,10 +54,16 @@ type UseBoardGestureParams = {
22
54
  moveMethod: MoveMethod;
23
55
  pieces: BoardPiece[];
24
56
  callbacks: GestureCallbacks;
57
+ richCallbacks?: RichCallbacks;
58
+ premoveCallbacks?: PremoveCallbacks;
59
+ /** Whether premoves are enabled */
60
+ premovesEnabled?: boolean;
25
61
  /** Currently selected square (for tap-to-move second tap) */
26
62
  selectedSquare: string | null;
27
63
  /** Legal move targets from the currently selected piece */
28
64
  legalMoves: LegalMoveTarget[];
65
+ /** Whose turn it is ('w' or 'b') — used for premove detection */
66
+ currentTurn?: 'w' | 'b';
29
67
  };
30
68
 
31
69
  type UseBoardGestureReturn = {
@@ -33,6 +71,24 @@ type UseBoardGestureReturn = {
33
71
  gestureState: GestureState;
34
72
  };
35
73
 
74
+ // ---------------------------------------------------------------------------
75
+ // Helpers
76
+ // ---------------------------------------------------------------------------
77
+
78
+ /** Check if a piece color matches the current player turn */
79
+ function isPieceTurn(pieceColor: 'w' | 'b', currentTurn: 'w' | 'b'): boolean {
80
+ return pieceColor === currentTurn;
81
+ }
82
+
83
+ /** Map piece color char to player ChessColor */
84
+ function pieceColorToPlayer(color: 'w' | 'b'): ChessColor {
85
+ return color === 'w' ? 'white' : 'black';
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Hook
90
+ // ---------------------------------------------------------------------------
91
+
36
92
  /**
37
93
  * Single centralized gesture handler for the entire board.
38
94
  *
@@ -51,6 +107,13 @@ type UseBoardGestureReturn = {
51
107
  * gestureEnabled, player, or moveMethod change). Frequently-changing data
52
108
  * (pieces, selectedSquare, legalMoves) is read from refs via runOnJS bridge
53
109
  * functions, avoiding costly gesture teardown/rebuild on every move.
110
+ *
111
+ * v0.2.0 additions:
112
+ * - Rich callbacks (onPieceClick, onSquareClick, onPieceDragBegin, onPieceDragEnd)
113
+ * - Drag target square tracking (shared value for DragTargetHighlight)
114
+ * - Premove support (queue move when not your turn)
115
+ * - Haptic feedback via callback
116
+ * - Long press detection for onSquareLongPress
54
117
  */
55
118
  export function useBoardGesture({
56
119
  squareSize,
@@ -60,8 +123,12 @@ export function useBoardGesture({
60
123
  moveMethod,
61
124
  pieces,
62
125
  callbacks,
126
+ richCallbacks,
127
+ premoveCallbacks,
128
+ premovesEnabled = false,
63
129
  selectedSquare,
64
130
  legalMoves,
131
+ currentTurn,
65
132
  }: UseBoardGestureParams): UseBoardGestureReturn {
66
133
  // Shared values for drag tracking — updated on UI thread only
67
134
  const activeSquare = useSharedValue<string | null>(null);
@@ -69,6 +136,7 @@ export function useBoardGesture({
69
136
  const dragY = useSharedValue(0);
70
137
  const isDragging = useSharedValue(false);
71
138
  const dragPieceCode = useSharedValue<string | null>(null);
139
+ const dragTargetSquare = useSharedValue<string | null>(null);
72
140
 
73
141
  const gestureState: GestureState = {
74
142
  activeSquare,
@@ -76,6 +144,7 @@ export function useBoardGesture({
76
144
  dragY,
77
145
  isDragging,
78
146
  dragPieceCode,
147
+ dragTargetSquare,
79
148
  };
80
149
 
81
150
  // --- Refs for frequently-changing data (read from JS thread via runOnJS) ---
@@ -92,6 +161,18 @@ export function useBoardGesture({
92
161
  const callbacksRef = useRef(callbacks);
93
162
  callbacksRef.current = callbacks;
94
163
 
164
+ const richCallbacksRef = useRef(richCallbacks);
165
+ richCallbacksRef.current = richCallbacks;
166
+
167
+ const premoveCallbacksRef = useRef(premoveCallbacks);
168
+ premoveCallbacksRef.current = premoveCallbacks;
169
+
170
+ const currentTurnRef = useRef(currentTurn);
171
+ currentTurnRef.current = currentTurn;
172
+
173
+ // Track the piece being dragged for rich drag-end callback
174
+ const draggedPieceRef = useRef<{ square: string; code: PieceCode } | null>(null);
175
+
95
176
  // --- JS-thread bridge functions called from worklets via runOnJS ---
96
177
  // These read current values from refs, so they always have fresh data.
97
178
 
@@ -101,14 +182,21 @@ export function useBoardGesture({
101
182
  const currentSelected = selectedSquareRef.current;
102
183
  const currentLegalMoves = legalMovesRef.current;
103
184
  const cbs = callbacksRef.current;
185
+ const rich = richCallbacksRef.current;
104
186
  const canClick = moveMethod !== 'drag';
105
187
 
106
188
  // Build lookup for the current touch
107
189
  const piece = currentPieces.find((p) => p.square === square);
108
190
  const isPlayerPiece = piece
109
- ? player === 'both' || (piece.color === 'w' ? 'white' : 'black') === player
191
+ ? player === 'both' || pieceColorToPlayer(piece.color) === player
110
192
  : false;
111
193
 
194
+ // Check if it's this piece's turn (for premove detection)
195
+ const turn = currentTurnRef.current;
196
+ const isOwnTurn = piece && turn
197
+ ? isPieceTurn(piece.color, turn)
198
+ : true; // default to true if turn not tracked
199
+
112
200
  // Click-to-move: second tap on a legal target square
113
201
  const legalSquares = new Set(currentLegalMoves.map((m) => m.square));
114
202
  if (canClick && currentSelected && legalSquares.has(square)) {
@@ -116,25 +204,93 @@ export function useBoardGesture({
116
204
  activeSquare.value = null;
117
205
  isDragging.value = false;
118
206
  dragPieceCode.value = null;
207
+ dragTargetSquare.value = null;
208
+ rich?.onHaptic?.('move');
119
209
  return;
120
210
  }
121
211
 
122
212
  if (isPlayerPiece && piece) {
123
- // Tapped/started dragging a player piece
213
+ // Premove: player piece but not their turn
214
+ if (premovesEnabled && !isOwnTurn) {
215
+ // If there's already a selected square, this tap completes a premove
216
+ if (currentSelected && currentSelected !== square) {
217
+ premoveCallbacksRef.current?.onPremoveSet?.({
218
+ from: currentSelected,
219
+ to: square,
220
+ });
221
+ cbs.onSelectionCleared();
222
+ activeSquare.value = null;
223
+ dragPieceCode.value = null;
224
+ dragTargetSquare.value = null;
225
+ return;
226
+ }
227
+ // First tap: select the piece for premove
228
+ activeSquare.value = square;
229
+ dragX.value = touchX;
230
+ dragY.value = touchY;
231
+ dragPieceCode.value = piece.code;
232
+ cbs.onPieceSelected(square);
233
+ rich?.onPieceClick?.(square, piece.code as PieceCode);
234
+ rich?.onHaptic?.('select');
235
+ return;
236
+ }
237
+
238
+ // Normal case: tapped/started dragging a player piece on their turn
124
239
  activeSquare.value = square;
125
240
  dragX.value = touchX;
126
241
  dragY.value = touchY;
127
242
  dragPieceCode.value = piece.code;
243
+ draggedPieceRef.current = { square, code: piece.code as PieceCode };
128
244
  cbs.onPieceSelected(square);
245
+
246
+ // Rich callbacks
247
+ rich?.onPieceClick?.(square, piece.code as PieceCode);
248
+ rich?.onHaptic?.('select');
129
249
  } else {
130
- // Tapped empty square or opponent piece — clear selection
250
+ // Tapped empty square or opponent piece
251
+
252
+ // If premoves enabled and there's a selection, check if this is a premove target
253
+ if (premovesEnabled && currentSelected && !isOwnTurn) {
254
+ premoveCallbacksRef.current?.onPremoveSet?.({
255
+ from: currentSelected,
256
+ to: square,
257
+ });
258
+ cbs.onSelectionCleared();
259
+ activeSquare.value = null;
260
+ dragPieceCode.value = null;
261
+ dragTargetSquare.value = null;
262
+ return;
263
+ }
264
+
131
265
  activeSquare.value = null;
132
266
  dragPieceCode.value = null;
267
+ dragTargetSquare.value = null;
133
268
  if (currentSelected) {
134
269
  cbs.onSelectionCleared();
135
270
  }
271
+
272
+ // Rich callback: square click (empty square or opponent piece)
273
+ rich?.onSquareClick?.(square);
136
274
  }
137
- }, [squareSize, orientation, player, moveMethod, activeSquare, dragX, dragY, isDragging, dragPieceCode]);
275
+ }, [squareSize, orientation, player, moveMethod, premovesEnabled, activeSquare, dragX, dragY, isDragging, dragPieceCode, dragTargetSquare]);
276
+
277
+ const handleDragStart = useCallback((touchX: number, touchY: number) => {
278
+ const rich = richCallbacksRef.current;
279
+ const dragged = draggedPieceRef.current;
280
+ if (dragged) {
281
+ rich?.onPieceDragBegin?.(dragged.square, dragged.code);
282
+ }
283
+ // Update drag target to current square
284
+ const square = xyToSquare(touchX, touchY, squareSize, orientation);
285
+ dragTargetSquare.value = square;
286
+ }, [squareSize, orientation, dragTargetSquare]);
287
+
288
+ const handleDragUpdate = useCallback((touchX: number, touchY: number) => {
289
+ // Update drag target square (for highlight). This runs on JS thread
290
+ // but is called from worklet via runOnJS only when the square changes.
291
+ const square = xyToSquare(touchX, touchY, squareSize, orientation);
292
+ dragTargetSquare.value = square;
293
+ }, [squareSize, orientation, dragTargetSquare]);
138
294
 
139
295
  const handleEnd = useCallback((touchX: number, touchY: number) => {
140
296
  const fromSquare = activeSquare.value;
@@ -142,18 +298,50 @@ export function useBoardGesture({
142
298
 
143
299
  const toSquare = xyToSquare(touchX, touchY, squareSize, orientation);
144
300
  isDragging.value = false;
301
+ dragTargetSquare.value = null;
302
+
303
+ const rich = richCallbacksRef.current;
304
+
305
+ // Fire drag end callback
306
+ const dragged = draggedPieceRef.current;
307
+ if (dragged) {
308
+ rich?.onPieceDragEnd?.(toSquare, dragged.code);
309
+ draggedPieceRef.current = null;
310
+ }
145
311
 
146
312
  if (fromSquare !== toSquare) {
147
- callbacksRef.current.onPieceMoved(fromSquare, toSquare);
313
+ // Check if this is a premove (not your turn)
314
+ const turn = currentTurnRef.current;
315
+ const piece = piecesRef.current.find((p) => p.square === fromSquare);
316
+ const isOwnTurn = piece && turn ? isPieceTurn(piece.color, turn) : true;
317
+
318
+ if (premovesEnabled && !isOwnTurn) {
319
+ premoveCallbacksRef.current?.onPremoveSet?.({
320
+ from: fromSquare,
321
+ to: toSquare,
322
+ });
323
+ } else {
324
+ callbacksRef.current.onPieceMoved(fromSquare, toSquare);
325
+ }
148
326
  }
149
327
 
150
328
  activeSquare.value = null;
151
329
  dragPieceCode.value = null;
152
- }, [squareSize, orientation, activeSquare, isDragging, dragPieceCode]);
330
+ }, [squareSize, orientation, activeSquare, isDragging, dragPieceCode, dragTargetSquare, premovesEnabled]);
331
+
332
+ // Long press handler (separate gesture, composed with pan)
333
+ const handleLongPress = useCallback((touchX: number, touchY: number) => {
334
+ const square = xyToSquare(touchX, touchY, squareSize, orientation);
335
+ richCallbacksRef.current?.onSquareLongPress?.(square);
336
+ }, [squareSize, orientation]);
153
337
 
154
338
  // --- Build the gesture (STABLE — only changes on layout/config changes) ---
155
339
  const canDrag = moveMethod !== 'click';
156
340
 
341
+ // Track the last drag target square to avoid redundant runOnJS calls
342
+ const lastDragTargetCol = useSharedValue(-1);
343
+ const lastDragTargetRow = useSharedValue(-1);
344
+
157
345
  const gesture = useMemo(() => {
158
346
  return Gesture.Pan()
159
347
  .enabled(gestureEnabled)
@@ -163,10 +351,11 @@ export function useBoardGesture({
163
351
  // Bridge to JS for piece lookup + selection logic
164
352
  runOnJS(handleBegin)(e.x, e.y);
165
353
  })
166
- .onStart(() => {
354
+ .onStart((e) => {
167
355
  'worklet';
168
356
  if (!canDrag || !activeSquare.value) return;
169
357
  isDragging.value = true;
358
+ runOnJS(handleDragStart)(e.x, e.y);
170
359
  })
171
360
  .onUpdate((e) => {
172
361
  'worklet';
@@ -174,6 +363,15 @@ export function useBoardGesture({
174
363
  // Only 2 shared value writes — no JS bridge, no re-renders
175
364
  dragX.value = e.x;
176
365
  dragY.value = e.y;
366
+
367
+ // Update drag target square (only when square changes to minimize JS bridge calls)
368
+ const col = Math.max(0, Math.min(7, Math.floor(e.x / squareSize)));
369
+ const row = Math.max(0, Math.min(7, Math.floor(e.y / squareSize)));
370
+ if (col !== lastDragTargetCol.value || row !== lastDragTargetRow.value) {
371
+ lastDragTargetCol.value = col;
372
+ lastDragTargetRow.value = row;
373
+ runOnJS(handleDragUpdate)(e.x, e.y);
374
+ }
177
375
  })
178
376
  .onEnd((e) => {
179
377
  'worklet';
@@ -184,18 +382,31 @@ export function useBoardGesture({
184
382
  'worklet';
185
383
  // Safety reset if gesture was interrupted
186
384
  isDragging.value = false;
385
+ dragTargetSquare.value = null;
386
+ lastDragTargetCol.value = -1;
387
+ lastDragTargetRow.value = -1;
187
388
  });
188
389
  }, [
189
390
  gestureEnabled,
190
391
  canDrag,
392
+ squareSize,
191
393
  handleBegin,
394
+ handleDragStart,
395
+ handleDragUpdate,
192
396
  handleEnd,
193
397
  // Shared values are stable refs — listed for exhaustive-deps but don't cause recreations
194
398
  activeSquare,
195
399
  dragX,
196
400
  dragY,
197
401
  isDragging,
402
+ dragTargetSquare,
403
+ lastDragTargetCol,
404
+ lastDragTargetRow,
198
405
  ]);
199
406
 
407
+ // Compose with long press if the consumer wants it
408
+ // For now, long press is detected via a separate gesture that runs simultaneously.
409
+ // This is done at the board level if needed — keeping the pan gesture clean here.
410
+
200
411
  return { gesture, gestureState };
201
412
  }
@@ -0,0 +1,59 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ import type { PremoveData } from './types';
4
+
5
+ type UsePremoveReturn = {
6
+ /** The currently queued premove (only one at a time, like lichess) */
7
+ premove: PremoveData | null;
8
+ /** Queue a premove. Replaces any existing premove. */
9
+ setPremove: (premove: PremoveData) => void;
10
+ /** Clear the queued premove */
11
+ clearPremove: () => void;
12
+ /**
13
+ * Try to execute the queued premove.
14
+ * Returns the premove if one was queued (so the caller can apply it),
15
+ * then clears the queue. Returns null if no premove was queued.
16
+ */
17
+ consumePremove: () => PremoveData | null;
18
+ };
19
+
20
+ /**
21
+ * Manages a single premove queue (one premove at a time).
22
+ *
23
+ * Premoves work like lichess:
24
+ * 1. When it's not your turn, you can drag/click a move — it queues as a premove
25
+ * 2. The premove squares are highlighted with a distinct color
26
+ * 3. When your turn begins (FEN changes and it's now your turn), the premove
27
+ * is automatically attempted via the board's internal chess.js
28
+ * 4. If the premove is legal, it's applied; if not, it's silently discarded
29
+ *
30
+ * The board.tsx orchestrates this: it calls consumePremove() in a useEffect
31
+ * that watches the FEN (turn change), then attempts the move.
32
+ */
33
+ export function usePremove(): UsePremoveReturn {
34
+ const [premove, setPremoveState] = useState<PremoveData | null>(null);
35
+
36
+ const setPremove = useCallback((pm: PremoveData) => {
37
+ setPremoveState(pm);
38
+ }, []);
39
+
40
+ const clearPremove = useCallback(() => {
41
+ setPremoveState(null);
42
+ }, []);
43
+
44
+ const consumePremove = useCallback((): PremoveData | null => {
45
+ let consumed: PremoveData | null = null;
46
+ setPremoveState((current) => {
47
+ consumed = current;
48
+ return null;
49
+ });
50
+ return consumed;
51
+ }, []);
52
+
53
+ return {
54
+ premove,
55
+ setPremove,
56
+ clearPremove,
57
+ consumePremove,
58
+ };
59
+ }