react-native-chess-kit 0.5.0 → 0.5.2

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 (62) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +168 -168
  3. package/lib/commonjs/board-annotations.js +8 -8
  4. package/lib/commonjs/board-arrows.js +2 -2
  5. package/lib/commonjs/board-arrows.js.map +1 -1
  6. package/lib/commonjs/board-background.js +5 -5
  7. package/lib/commonjs/board-coordinates.js +8 -8
  8. package/lib/commonjs/board-drag-ghost.js +10 -10
  9. package/lib/commonjs/board-highlights.js +15 -15
  10. package/lib/commonjs/board-legal-dots.js +5 -5
  11. package/lib/commonjs/board-piece.js +25 -25
  12. package/lib/commonjs/board-pieces.js +6 -6
  13. package/lib/commonjs/board.js +24 -24
  14. package/lib/commonjs/promotion-picker.js +8 -8
  15. package/lib/commonjs/static-board.js +7 -7
  16. package/lib/commonjs/use-board-gesture.js +52 -33
  17. package/lib/commonjs/use-board-gesture.js.map +1 -1
  18. package/lib/commonjs/use-board-pieces.js +15 -15
  19. package/lib/commonjs/use-board-state.js +8 -8
  20. package/lib/commonjs/use-premove.js +12 -12
  21. package/lib/module/board-annotations.js +8 -8
  22. package/lib/module/board-arrows.js +2 -2
  23. package/lib/module/board-arrows.js.map +1 -1
  24. package/lib/module/board-background.js +5 -5
  25. package/lib/module/board-coordinates.js +8 -8
  26. package/lib/module/board-drag-ghost.js +10 -10
  27. package/lib/module/board-highlights.js +15 -15
  28. package/lib/module/board-legal-dots.js +5 -5
  29. package/lib/module/board-piece.js +25 -25
  30. package/lib/module/board-pieces.js +6 -6
  31. package/lib/module/board.js +24 -24
  32. package/lib/module/promotion-picker.js +8 -8
  33. package/lib/module/static-board.js +7 -7
  34. package/lib/module/use-board-gesture.js +52 -33
  35. package/lib/module/use-board-gesture.js.map +1 -1
  36. package/lib/module/use-board-pieces.js +15 -15
  37. package/lib/module/use-board-state.js +8 -8
  38. package/lib/module/use-premove.js +12 -12
  39. package/lib/typescript/use-board-gesture.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/board-annotations.tsx +147 -147
  42. package/src/board-arrows.tsx +2 -2
  43. package/src/board-background.tsx +46 -46
  44. package/src/board-coordinates.tsx +192 -192
  45. package/src/board-drag-ghost.tsx +132 -132
  46. package/src/board-highlights.tsx +226 -226
  47. package/src/board-legal-dots.tsx +73 -73
  48. package/src/board-piece.tsx +160 -160
  49. package/src/board-pieces.tsx +63 -63
  50. package/src/board.tsx +685 -685
  51. package/src/constants.ts +103 -103
  52. package/src/index.ts +101 -101
  53. package/src/pieces/default-pieces.tsx +383 -383
  54. package/src/pieces/index.ts +1 -1
  55. package/src/promotion-picker.tsx +147 -147
  56. package/src/static-board.tsx +187 -187
  57. package/src/themes.ts +129 -129
  58. package/src/types.ts +373 -373
  59. package/src/use-board-gesture.ts +429 -412
  60. package/src/use-board-pieces.ts +158 -158
  61. package/src/use-board-state.ts +111 -111
  62. package/src/use-premove.ts +59 -59
@@ -1,412 +1,429 @@
1
- import { useMemo, useCallback, useRef } from 'react';
2
- import { Gesture } from 'react-native-gesture-handler';
3
- import {
4
- useSharedValue,
5
- runOnJS,
6
- } from 'react-native-reanimated';
7
-
8
- import type {
9
- ChessColor,
10
- MoveMethod,
11
- BoardPiece,
12
- LegalMoveTarget,
13
- GestureState,
14
- PieceCode,
15
- HapticType,
16
- PremoveData,
17
- } from './types';
18
- import { xyToSquare } from './use-board-pieces';
19
-
20
- // ---------------------------------------------------------------------------
21
- // Callback types
22
- // ---------------------------------------------------------------------------
23
-
24
- type GestureCallbacks = {
25
- onPieceSelected: (square: string) => void;
26
- onPieceMoved: (from: string, to: string) => void;
27
- onSelectionCleared: () => void;
28
- };
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
-
49
- type UseBoardGestureParams = {
50
- squareSize: number;
51
- orientation: ChessColor;
52
- gestureEnabled: boolean;
53
- player: ChessColor | 'both';
54
- moveMethod: MoveMethod;
55
- pieces: BoardPiece[];
56
- callbacks: GestureCallbacks;
57
- richCallbacks?: RichCallbacks;
58
- premoveCallbacks?: PremoveCallbacks;
59
- /** Whether premoves are enabled */
60
- premovesEnabled?: boolean;
61
- /** Currently selected square (for tap-to-move second tap) */
62
- selectedSquare: string | null;
63
- /** Legal move targets from the currently selected piece */
64
- legalMoves: LegalMoveTarget[];
65
- /** Whose turn it is ('w' or 'b') — used for premove detection */
66
- currentTurn?: 'w' | 'b';
67
- };
68
-
69
- type UseBoardGestureReturn = {
70
- gesture: ReturnType<typeof Gesture.Pan>;
71
- gestureState: GestureState;
72
- };
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
-
92
- /**
93
- * Single centralized gesture handler for the entire board.
94
- *
95
- * Instead of 32 separate Gesture.Pan() handlers (one per piece), we use ONE
96
- * handler on the board container. Touch -> coordinate math -> which piece.
97
- *
98
- * Supports three modes:
99
- * - 'drag': drag piece to target square
100
- * - 'click': tap source piece, then tap target square
101
- * - 'both': drag or click (default)
102
- *
103
- * All drag position tracking uses shared values — zero JS bridge calls,
104
- * zero re-renders during drag. Only the final move triggers JS via runOnJS.
105
- *
106
- * The gesture object is STABLE (only recreated when squareSize, orientation,
107
- * gestureEnabled, player, or moveMethod change). Frequently-changing data
108
- * (pieces, selectedSquare, legalMoves) is read from refs via runOnJS bridge
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
117
- */
118
- export function useBoardGesture({
119
- squareSize,
120
- orientation,
121
- gestureEnabled,
122
- player,
123
- moveMethod,
124
- pieces,
125
- callbacks,
126
- richCallbacks,
127
- premoveCallbacks,
128
- premovesEnabled = false,
129
- selectedSquare,
130
- legalMoves,
131
- currentTurn,
132
- }: UseBoardGestureParams): UseBoardGestureReturn {
133
- // Shared values for drag tracking — updated on UI thread only
134
- const activeSquare = useSharedValue<string | null>(null);
135
- const dragX = useSharedValue(0);
136
- const dragY = useSharedValue(0);
137
- const isDragging = useSharedValue(false);
138
- const dragPieceCode = useSharedValue<string | null>(null);
139
- const dragTargetSquare = useSharedValue<string | null>(null);
140
-
141
- const gestureState: GestureState = {
142
- activeSquare,
143
- dragX,
144
- dragY,
145
- isDragging,
146
- dragPieceCode,
147
- dragTargetSquare,
148
- };
149
-
150
- // --- Refs for frequently-changing data (read from JS thread via runOnJS) ---
151
- // These update every move but do NOT cause gesture object recreation.
152
- const piecesRef = useRef(pieces);
153
- piecesRef.current = pieces;
154
-
155
- const selectedSquareRef = useRef(selectedSquare);
156
- selectedSquareRef.current = selectedSquare;
157
-
158
- const legalMovesRef = useRef(legalMoves);
159
- legalMovesRef.current = legalMoves;
160
-
161
- const callbacksRef = useRef(callbacks);
162
- callbacksRef.current = callbacks;
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
-
176
- // --- JS-thread bridge functions called from worklets via runOnJS ---
177
- // These read current values from refs, so they always have fresh data.
178
-
179
- const handleBegin = useCallback((touchX: number, touchY: number) => {
180
- const square = xyToSquare(touchX, touchY, squareSize, orientation);
181
- const currentPieces = piecesRef.current;
182
- const currentSelected = selectedSquareRef.current;
183
- const currentLegalMoves = legalMovesRef.current;
184
- const cbs = callbacksRef.current;
185
- const rich = richCallbacksRef.current;
186
- const canClick = moveMethod !== 'drag';
187
-
188
- // Build lookup for the current touch
189
- const piece = currentPieces.find((p) => p.square === square);
190
- const isPlayerPiece = piece
191
- ? player === 'both' || pieceColorToPlayer(piece.color) === player
192
- : false;
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
-
200
- // Click-to-move: second tap on a legal target square
201
- const legalSquares = new Set(currentLegalMoves.map((m) => m.square));
202
- if (canClick && currentSelected && legalSquares.has(square)) {
203
- cbs.onPieceMoved(currentSelected, square);
204
- activeSquare.value = null;
205
- isDragging.value = false;
206
- dragPieceCode.value = null;
207
- dragTargetSquare.value = null;
208
- rich?.onHaptic?.('move');
209
- return;
210
- }
211
-
212
- if (isPlayerPiece && 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
239
- activeSquare.value = square;
240
- dragX.value = touchX;
241
- dragY.value = touchY;
242
- dragPieceCode.value = piece.code;
243
- draggedPieceRef.current = { square, code: piece.code as PieceCode };
244
- cbs.onPieceSelected(square);
245
-
246
- // Rich callbacks
247
- rich?.onPieceClick?.(square, piece.code as PieceCode);
248
- rich?.onHaptic?.('select');
249
- } else {
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
-
265
- activeSquare.value = null;
266
- dragPieceCode.value = null;
267
- dragTargetSquare.value = null;
268
- if (currentSelected) {
269
- cbs.onSelectionCleared();
270
- }
271
-
272
- // Rich callback: square click (empty square or opponent piece)
273
- rich?.onSquareClick?.(square);
274
- }
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]);
294
-
295
- const handleEnd = useCallback((touchX: number, touchY: number) => {
296
- const fromSquare = activeSquare.value;
297
- if (!fromSquare) return;
298
-
299
- const toSquare = xyToSquare(touchX, touchY, squareSize, orientation);
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
- }
311
-
312
- if (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
- }
326
- }
327
-
328
- activeSquare.value = null;
329
- dragPieceCode.value = null;
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]);
337
-
338
- // --- Build the gesture (STABLE only changes on layout/config changes) ---
339
- const canDrag = moveMethod !== 'click';
340
-
341
- // Track the last drag target square to avoid redundant runOnJS calls
342
- const lastDragTargetCol = useSharedValue(-1);
343
- const lastDragTargetRow = useSharedValue(-1);
344
-
345
- const gesture = useMemo(() => {
346
- return Gesture.Pan()
347
- .enabled(gestureEnabled)
348
- .minDistance(0) // Also detect taps (zero-distance pans)
349
- .onBegin((e) => {
350
- 'worklet';
351
- // Bridge to JS for piece lookup + selection logic
352
- runOnJS(handleBegin)(e.x, e.y);
353
- })
354
- .onStart((e) => {
355
- 'worklet';
356
- if (!canDrag || !activeSquare.value) return;
357
- isDragging.value = true;
358
- runOnJS(handleDragStart)(e.x, e.y);
359
- })
360
- .onUpdate((e) => {
361
- 'worklet';
362
- if (!canDrag || !isDragging.value) return;
363
- // Only 2 shared value writes no JS bridge, no re-renders
364
- dragX.value = e.x;
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
- }
375
- })
376
- .onEnd((e) => {
377
- 'worklet';
378
- if (!isDragging.value || !activeSquare.value) return;
379
- runOnJS(handleEnd)(e.x, e.y);
380
- })
381
- .onFinalize(() => {
382
- 'worklet';
383
- // Safety reset if gesture was interrupted
384
- isDragging.value = false;
385
- dragTargetSquare.value = null;
386
- lastDragTargetCol.value = -1;
387
- lastDragTargetRow.value = -1;
388
- });
389
- }, [
390
- gestureEnabled,
391
- canDrag,
392
- squareSize,
393
- handleBegin,
394
- handleDragStart,
395
- handleDragUpdate,
396
- handleEnd,
397
- // Shared values are stable refs — listed for exhaustive-deps but don't cause recreations
398
- activeSquare,
399
- dragX,
400
- dragY,
401
- isDragging,
402
- dragTargetSquare,
403
- lastDragTargetCol,
404
- lastDragTargetRow,
405
- ]);
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
-
411
- return { gesture, gestureState };
412
- }
1
+ import { useMemo, useCallback, useRef } from 'react';
2
+ import { Gesture } from 'react-native-gesture-handler';
3
+ import {
4
+ useSharedValue,
5
+ runOnJS,
6
+ } from 'react-native-reanimated';
7
+
8
+ import type {
9
+ ChessColor,
10
+ MoveMethod,
11
+ BoardPiece,
12
+ LegalMoveTarget,
13
+ GestureState,
14
+ PieceCode,
15
+ HapticType,
16
+ PremoveData,
17
+ } from './types';
18
+ import { xyToSquare } from './use-board-pieces';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Callback types
22
+ // ---------------------------------------------------------------------------
23
+
24
+ type GestureCallbacks = {
25
+ onPieceSelected: (square: string) => void;
26
+ onPieceMoved: (from: string, to: string) => void;
27
+ onSelectionCleared: () => void;
28
+ };
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
+
49
+ type UseBoardGestureParams = {
50
+ squareSize: number;
51
+ orientation: ChessColor;
52
+ gestureEnabled: boolean;
53
+ player: ChessColor | 'both';
54
+ moveMethod: MoveMethod;
55
+ pieces: BoardPiece[];
56
+ callbacks: GestureCallbacks;
57
+ richCallbacks?: RichCallbacks;
58
+ premoveCallbacks?: PremoveCallbacks;
59
+ /** Whether premoves are enabled */
60
+ premovesEnabled?: boolean;
61
+ /** Currently selected square (for tap-to-move second tap) */
62
+ selectedSquare: string | null;
63
+ /** Legal move targets from the currently selected piece */
64
+ legalMoves: LegalMoveTarget[];
65
+ /** Whose turn it is ('w' or 'b') — used for premove detection */
66
+ currentTurn?: 'w' | 'b';
67
+ };
68
+
69
+ type UseBoardGestureReturn = {
70
+ gesture: ReturnType<typeof Gesture.Pan>;
71
+ gestureState: GestureState;
72
+ };
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
+
92
+ /**
93
+ * Single centralized gesture handler for the entire board.
94
+ *
95
+ * Instead of 32 separate Gesture.Pan() handlers (one per piece), we use ONE
96
+ * handler on the board container. Touch -> coordinate math -> which piece.
97
+ *
98
+ * Supports three modes:
99
+ * - 'drag': drag piece to target square
100
+ * - 'click': tap source piece, then tap target square
101
+ * - 'both': drag or click (default)
102
+ *
103
+ * All drag position tracking uses shared values — zero JS bridge calls,
104
+ * zero re-renders during drag. Only the final move triggers JS via runOnJS.
105
+ *
106
+ * The gesture object is STABLE (only recreated when squareSize, orientation,
107
+ * gestureEnabled, player, or moveMethod change). Frequently-changing data
108
+ * (pieces, selectedSquare, legalMoves) is read from refs via runOnJS bridge
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
117
+ */
118
+ export function useBoardGesture({
119
+ squareSize,
120
+ orientation,
121
+ gestureEnabled,
122
+ player,
123
+ moveMethod,
124
+ pieces,
125
+ callbacks,
126
+ richCallbacks,
127
+ premoveCallbacks,
128
+ premovesEnabled = false,
129
+ selectedSquare,
130
+ legalMoves,
131
+ currentTurn,
132
+ }: UseBoardGestureParams): UseBoardGestureReturn {
133
+ // Shared values for drag tracking — updated on UI thread only
134
+ const activeSquare = useSharedValue<string | null>(null);
135
+ const dragX = useSharedValue(0);
136
+ const dragY = useSharedValue(0);
137
+ const isDragging = useSharedValue(false);
138
+ const dragPieceCode = useSharedValue<string | null>(null);
139
+ const dragTargetSquare = useSharedValue<string | null>(null);
140
+
141
+ const gestureState: GestureState = {
142
+ activeSquare,
143
+ dragX,
144
+ dragY,
145
+ isDragging,
146
+ dragPieceCode,
147
+ dragTargetSquare,
148
+ };
149
+
150
+ // --- Refs for frequently-changing data (read from JS thread via runOnJS) ---
151
+ // These update every move but do NOT cause gesture object recreation.
152
+ const piecesRef = useRef(pieces);
153
+ piecesRef.current = pieces;
154
+
155
+ const selectedSquareRef = useRef(selectedSquare);
156
+ selectedSquareRef.current = selectedSquare;
157
+
158
+ const legalMovesRef = useRef(legalMoves);
159
+ legalMovesRef.current = legalMoves;
160
+
161
+ const callbacksRef = useRef(callbacks);
162
+ callbacksRef.current = callbacks;
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
+
176
+ // --- JS-thread bridge functions called from worklets via runOnJS ---
177
+ // These read current values from refs, so they always have fresh data.
178
+
179
+ const handleBegin = useCallback((touchX: number, touchY: number) => {
180
+ const square = xyToSquare(touchX, touchY, squareSize, orientation);
181
+ const currentPieces = piecesRef.current;
182
+ const currentSelected = selectedSquareRef.current;
183
+ const currentLegalMoves = legalMovesRef.current;
184
+ const cbs = callbacksRef.current;
185
+ const rich = richCallbacksRef.current;
186
+ const canClick = moveMethod !== 'drag';
187
+
188
+ // Build lookup for the current touch
189
+ const piece = currentPieces.find((p) => p.square === square);
190
+ const isPlayerPiece = piece
191
+ ? player === 'both' || pieceColorToPlayer(piece.color) === player
192
+ : false;
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
+
200
+ // Click-to-move: second tap on a legal target square
201
+ const legalSquares = new Set(currentLegalMoves.map((m) => m.square));
202
+ if (canClick && currentSelected && legalSquares.has(square)) {
203
+ cbs.onPieceMoved(currentSelected, square);
204
+ // Clear optimistic worklet value move is complete
205
+ activeSquare.value = null;
206
+ isDragging.value = false;
207
+ dragPieceCode.value = null;
208
+ dragTargetSquare.value = null;
209
+ rich?.onHaptic?.('move');
210
+ return;
211
+ }
212
+
213
+ // Chess.com-style deselect: tapping the already-selected piece toggles it off
214
+ if (canClick && currentSelected === square) {
215
+ cbs.onSelectionCleared();
216
+ activeSquare.value = null;
217
+ dragPieceCode.value = null;
218
+ dragTargetSquare.value = null;
219
+ return;
220
+ }
221
+
222
+ if (isPlayerPiece && piece) {
223
+ // Premove: player piece but not their turn
224
+ if (premovesEnabled && !isOwnTurn) {
225
+ // If there's already a selected square, this tap completes a premove
226
+ if (currentSelected && currentSelected !== square) {
227
+ premoveCallbacksRef.current?.onPremoveSet?.({
228
+ from: currentSelected,
229
+ to: square,
230
+ });
231
+ cbs.onSelectionCleared();
232
+ activeSquare.value = null;
233
+ dragPieceCode.value = null;
234
+ dragTargetSquare.value = null;
235
+ return;
236
+ }
237
+ // First tap: select the piece for premove
238
+ // activeSquare.value / dragX.value / dragY.value already set by onBegin worklet
239
+ dragPieceCode.value = piece.code;
240
+ cbs.onPieceSelected(square);
241
+ rich?.onPieceClick?.(square, piece.code as PieceCode);
242
+ rich?.onHaptic?.('select');
243
+ return;
244
+ }
245
+
246
+ // Normal case: tapped/started dragging a player piece on their turn
247
+ // activeSquare.value / dragX.value / dragY.value already set by onBegin worklet
248
+ dragPieceCode.value = piece.code;
249
+ draggedPieceRef.current = { square, code: piece.code as PieceCode };
250
+ cbs.onPieceSelected(square);
251
+
252
+ // Rich callbacks
253
+ rich?.onPieceClick?.(square, piece.code as PieceCode);
254
+ rich?.onHaptic?.('select');
255
+ } else {
256
+ // Tapped empty square or opponent piece
257
+
258
+ // If premoves enabled and there's a selection, check if this is a premove target
259
+ if (premovesEnabled && currentSelected && !isOwnTurn) {
260
+ premoveCallbacksRef.current?.onPremoveSet?.({
261
+ from: currentSelected,
262
+ to: square,
263
+ });
264
+ cbs.onSelectionCleared();
265
+ activeSquare.value = null;
266
+ dragPieceCode.value = null;
267
+ dragTargetSquare.value = null;
268
+ return;
269
+ }
270
+
271
+ // Clear the optimistic worklet value — no valid piece here
272
+ activeSquare.value = null;
273
+ dragPieceCode.value = null;
274
+ dragTargetSquare.value = null;
275
+ if (currentSelected) {
276
+ cbs.onSelectionCleared();
277
+ }
278
+
279
+ // Rich callback: square click (empty square or opponent piece)
280
+ rich?.onSquareClick?.(square);
281
+ }
282
+ }, [squareSize, orientation, player, moveMethod, premovesEnabled, activeSquare, dragX, dragY, isDragging, dragPieceCode, dragTargetSquare]);
283
+
284
+ const handleDragStart = useCallback((touchX: number, touchY: number) => {
285
+ const rich = richCallbacksRef.current;
286
+ const dragged = draggedPieceRef.current;
287
+ if (dragged) {
288
+ rich?.onPieceDragBegin?.(dragged.square, dragged.code);
289
+ }
290
+ // Update drag target to current square
291
+ const square = xyToSquare(touchX, touchY, squareSize, orientation);
292
+ dragTargetSquare.value = square;
293
+ }, [squareSize, orientation, dragTargetSquare]);
294
+
295
+ const handleDragUpdate = useCallback((touchX: number, touchY: number) => {
296
+ // Update drag target square (for highlight). This runs on JS thread
297
+ // but is called from worklet via runOnJS only when the square changes.
298
+ const square = xyToSquare(touchX, touchY, squareSize, orientation);
299
+ dragTargetSquare.value = square;
300
+ }, [squareSize, orientation, dragTargetSquare]);
301
+
302
+ const handleEnd = useCallback((touchX: number, touchY: number) => {
303
+ const fromSquare = activeSquare.value;
304
+ if (!fromSquare) return;
305
+
306
+ const toSquare = xyToSquare(touchX, touchY, squareSize, orientation);
307
+ isDragging.value = false;
308
+ dragTargetSquare.value = null;
309
+
310
+ const rich = richCallbacksRef.current;
311
+
312
+ // Fire drag end callback
313
+ const dragged = draggedPieceRef.current;
314
+ if (dragged) {
315
+ rich?.onPieceDragEnd?.(toSquare, dragged.code);
316
+ draggedPieceRef.current = null;
317
+ }
318
+
319
+ if (fromSquare !== toSquare) {
320
+ // Check if this is a premove (not your turn)
321
+ const turn = currentTurnRef.current;
322
+ const piece = piecesRef.current.find((p) => p.square === fromSquare);
323
+ const isOwnTurn = piece && turn ? isPieceTurn(piece.color, turn) : true;
324
+
325
+ if (premovesEnabled && !isOwnTurn) {
326
+ premoveCallbacksRef.current?.onPremoveSet?.({
327
+ from: fromSquare,
328
+ to: toSquare,
329
+ });
330
+ } else {
331
+ callbacksRef.current.onPieceMoved(fromSquare, toSquare);
332
+ }
333
+
334
+ // Move completed clear selection
335
+ activeSquare.value = null;
336
+ dragPieceCode.value = null;
337
+ }
338
+ // When fromSquare === toSquare (tap with no movement), keep selection alive
339
+ // so the user can complete a click-to-move on the next tap.
340
+ }, [squareSize, orientation, activeSquare, isDragging, dragPieceCode, dragTargetSquare, premovesEnabled]);
341
+
342
+ // Long press handler (separate gesture, composed with pan)
343
+ const handleLongPress = useCallback((touchX: number, touchY: number) => {
344
+ const square = xyToSquare(touchX, touchY, squareSize, orientation);
345
+ richCallbacksRef.current?.onSquareLongPress?.(square);
346
+ }, [squareSize, orientation]);
347
+
348
+ // --- Build the gesture (STABLE — only changes on layout/config changes) ---
349
+ const canDrag = moveMethod !== 'click';
350
+
351
+ // Track the last drag target square to avoid redundant runOnJS calls
352
+ const lastDragTargetCol = useSharedValue(-1);
353
+ const lastDragTargetRow = useSharedValue(-1);
354
+
355
+ const gesture = useMemo(() => {
356
+ return Gesture.Pan()
357
+ .enabled(gestureEnabled)
358
+ .minDistance(0) // Also detect taps (zero-distance pans)
359
+ .onBegin((e) => {
360
+ 'worklet';
361
+ // Set shared values IMMEDIATELY on the UI thread so onStart can read them
362
+ // synchronously. xyToSquare is tagged 'worklet' — safe to call here.
363
+ // handleBegin (JS thread) will CLEAR activeSquare if the square is invalid.
364
+ const sq = xyToSquare(e.x, e.y, squareSize, orientation);
365
+ activeSquare.value = sq;
366
+ dragX.value = e.x;
367
+ dragY.value = e.y;
368
+ // Bridge to JS for piece lookup + selection logic
369
+ runOnJS(handleBegin)(e.x, e.y);
370
+ })
371
+ .onStart((e) => {
372
+ 'worklet';
373
+ if (!canDrag || !activeSquare.value) return;
374
+ isDragging.value = true;
375
+ runOnJS(handleDragStart)(e.x, e.y);
376
+ })
377
+ .onUpdate((e) => {
378
+ 'worklet';
379
+ if (!canDrag || !isDragging.value) return;
380
+ // Only 2 shared value writes — no JS bridge, no re-renders
381
+ dragX.value = e.x;
382
+ dragY.value = e.y;
383
+
384
+ // Update drag target square (only when square changes to minimize JS bridge calls)
385
+ const col = Math.max(0, Math.min(7, Math.floor(e.x / squareSize)));
386
+ const row = Math.max(0, Math.min(7, Math.floor(e.y / squareSize)));
387
+ if (col !== lastDragTargetCol.value || row !== lastDragTargetRow.value) {
388
+ lastDragTargetCol.value = col;
389
+ lastDragTargetRow.value = row;
390
+ runOnJS(handleDragUpdate)(e.x, e.y);
391
+ }
392
+ })
393
+ .onEnd((e) => {
394
+ 'worklet';
395
+ if (!isDragging.value || !activeSquare.value) return;
396
+ runOnJS(handleEnd)(e.x, e.y);
397
+ })
398
+ .onFinalize(() => {
399
+ 'worklet';
400
+ // Safety reset if gesture was interrupted
401
+ isDragging.value = false;
402
+ dragTargetSquare.value = null;
403
+ lastDragTargetCol.value = -1;
404
+ lastDragTargetRow.value = -1;
405
+ });
406
+ }, [
407
+ gestureEnabled,
408
+ canDrag,
409
+ squareSize,
410
+ handleBegin,
411
+ handleDragStart,
412
+ handleDragUpdate,
413
+ handleEnd,
414
+ // Shared values are stable refs — listed for exhaustive-deps but don't cause recreations
415
+ activeSquare,
416
+ dragX,
417
+ dragY,
418
+ isDragging,
419
+ dragTargetSquare,
420
+ lastDragTargetCol,
421
+ lastDragTargetRow,
422
+ ]);
423
+
424
+ // Compose with long press if the consumer wants it
425
+ // For now, long press is detected via a separate gesture that runs simultaneously.
426
+ // This is done at the board level if needed — keeping the pan gesture clean here.
427
+
428
+ return { gesture, gestureState };
429
+ }