koin.js 1.0.12 → 1.0.14

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.
package/dist/index.js CHANGED
@@ -2282,7 +2282,8 @@ var SIX_BUTTON_LAYOUT = {
2282
2282
  { type: "y", label: "A", x: 72, y: 64, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2283
2283
  { type: "b", label: "B", x: 82, y: 60, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2284
2284
  { type: "a", label: "C", x: 92, y: 56, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2285
- { type: "start", label: "START", x: 50, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2285
+ { type: "select", label: "SELECT", x: SELECT_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" },
2286
+ { type: "start", label: "START", x: START_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2286
2287
  ]
2287
2288
  };
2288
2289
  var SATURN_LAYOUT = {
@@ -2301,7 +2302,8 @@ var SATURN_LAYOUT = {
2301
2302
  // Triggers (L2/R2 for Saturn L/R)
2302
2303
  { type: "l2", label: "L", x: 8, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2303
2304
  { type: "r2", label: "R", x: 92, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2304
- { type: "start", label: "START", x: 50, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2305
+ { type: "select", label: "SELECT", x: SELECT_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" },
2306
+ { type: "start", label: "START", x: START_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2305
2307
  ]
2306
2308
  };
2307
2309
  var NEOGEO_LAYOUT = {
@@ -2350,9 +2352,10 @@ var N64_LAYOUT = {
2350
2352
  // Shoulders
2351
2353
  { type: "l", label: "L", x: 8, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2352
2354
  { type: "r", label: "R", x: 92, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2353
- // Z trigger (use select as workaround since it's a unique button)
2354
- { type: "select", label: "Z", x: 8, y: 35, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2355
- { type: "start", label: "START", x: 50, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2355
+ // Z trigger (use l3 button for Z trigger)
2356
+ { type: "l3", label: "Z", x: 8, y: 35, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2357
+ { type: "select", label: "SELECT", x: SELECT_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" },
2358
+ { type: "start", label: "START", x: START_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2356
2359
  ]
2357
2360
  };
2358
2361
  var DREAMCAST_LAYOUT = {
@@ -2367,7 +2370,8 @@ var DREAMCAST_LAYOUT = {
2367
2370
  // Triggers
2368
2371
  { type: "l", label: "L", x: 8, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2369
2372
  { type: "r", label: "R", x: 92, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2370
- { type: "start", label: "START", x: 50, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2373
+ { type: "select", label: "SELECT", x: SELECT_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" },
2374
+ { type: "start", label: "START", x: START_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2371
2375
  ]
2372
2376
  };
2373
2377
  function getLayoutForSystem(system) {
@@ -2389,26 +2393,49 @@ function getLayoutForSystem(system) {
2389
2393
  if (s.includes("ATARI")) return TWO_BUTTON_LAYOUT;
2390
2394
  return TWO_BUTTON_LAYOUT;
2391
2395
  }
2392
- var DRAG_HOLD_DELAY = 350;
2396
+
2397
+ // src/components/VirtualController/utils/dragConstraints.ts
2398
+ function constrainToViewport({
2399
+ newXPercent,
2400
+ newYPercent,
2401
+ elementSize,
2402
+ containerWidth,
2403
+ containerHeight
2404
+ }) {
2405
+ const xMargin = elementSize / 2 / containerWidth * 100;
2406
+ const yMargin = elementSize / 2 / containerHeight * 100;
2407
+ return {
2408
+ x: Math.max(xMargin, Math.min(100 - xMargin, newXPercent)),
2409
+ y: Math.max(yMargin, Math.min(100 - yMargin, newYPercent))
2410
+ };
2411
+ }
2412
+
2413
+ // src/components/VirtualController/hooks/useDrag.ts
2414
+ var DEFAULT_HOLD_DELAY = 350;
2415
+ var DEFAULT_CENTER_THRESHOLD = 0.4;
2393
2416
  var DRAG_MOVE_THRESHOLD = 10;
2394
- var DRAG_CENTER_THRESHOLD = 0.4;
2395
- function useTouchHandlers({
2396
- buttonType,
2397
- isSystemButton,
2398
- buttonSize,
2417
+ function useDrag({
2418
+ elementSize,
2399
2419
  displayX,
2400
2420
  displayY,
2401
2421
  containerWidth,
2402
2422
  containerHeight,
2403
- onPress,
2404
- onPressDown,
2405
- onRelease,
2406
- onPositionChange
2423
+ onPositionChange,
2424
+ holdDelay = DEFAULT_HOLD_DELAY,
2425
+ centerThreshold = DEFAULT_CENTER_THRESHOLD,
2426
+ onDragStart,
2427
+ onDragEnd
2407
2428
  }) {
2429
+ const [isDragging, setIsDragging] = React2.useState(false);
2408
2430
  const isDraggingRef = React2.useRef(false);
2409
- const dragStartRef = React2.useRef({ x: 0, y: 0 });
2410
2431
  const dragTimerRef = React2.useRef(null);
2411
2432
  const touchStartPosRef = React2.useRef({ x: 0, y: 0 });
2433
+ const dragStartRef = React2.useRef({
2434
+ elementX: 0,
2435
+ elementY: 0,
2436
+ touchX: 0,
2437
+ touchY: 0
2438
+ });
2412
2439
  const clearDragTimer = React2.useCallback(() => {
2413
2440
  if (dragTimerRef.current) {
2414
2441
  clearTimeout(dragTimerRef.current);
@@ -2418,23 +2445,129 @@ function useTouchHandlers({
2418
2445
  const startDragging = React2.useCallback(
2419
2446
  (touchX, touchY) => {
2420
2447
  isDraggingRef.current = true;
2448
+ setIsDragging(true);
2421
2449
  dragStartRef.current = {
2422
- x: touchX - displayX / 100 * containerWidth,
2423
- y: touchY - displayY / 100 * containerHeight
2450
+ elementX: displayX,
2451
+ elementY: displayY,
2452
+ touchX,
2453
+ touchY
2424
2454
  };
2425
2455
  if (navigator.vibrate) {
2426
2456
  navigator.vibrate([10, 30, 10]);
2427
2457
  }
2458
+ onDragStart?.();
2459
+ },
2460
+ [displayX, displayY, onDragStart]
2461
+ );
2462
+ const checkDragStart = React2.useCallback(
2463
+ (touchX, touchY, elementRect) => {
2464
+ if (!onPositionChange) return false;
2465
+ touchStartPosRef.current = { x: touchX, y: touchY };
2466
+ const centerX = elementRect.left + elementRect.width / 2;
2467
+ const centerY = elementRect.top + elementRect.height / 2;
2468
+ const distFromCenter = Math.sqrt(
2469
+ Math.pow(touchX - centerX, 2) + Math.pow(touchY - centerY, 2)
2470
+ );
2471
+ const centerRadius = elementSize * centerThreshold;
2472
+ if (distFromCenter < centerRadius) {
2473
+ dragTimerRef.current = setTimeout(() => {
2474
+ if (!isDraggingRef.current) {
2475
+ startDragging(touchX, touchY);
2476
+ }
2477
+ }, holdDelay);
2478
+ return true;
2479
+ }
2480
+ return false;
2481
+ },
2482
+ [onPositionChange, elementSize, centerThreshold, holdDelay, startDragging]
2483
+ );
2484
+ const checkMoveThreshold = React2.useCallback(
2485
+ (touchX, touchY) => {
2486
+ if (!onPositionChange || isDraggingRef.current) return false;
2487
+ const moveDistance = Math.sqrt(
2488
+ Math.pow(touchX - touchStartPosRef.current.x, 2) + Math.pow(touchY - touchStartPosRef.current.y, 2)
2489
+ );
2490
+ if (moveDistance > DRAG_MOVE_THRESHOLD) {
2491
+ clearDragTimer();
2492
+ startDragging(touchX, touchY);
2493
+ return true;
2494
+ }
2495
+ return false;
2496
+ },
2497
+ [onPositionChange, clearDragTimer, startDragging]
2498
+ );
2499
+ const handleDragMove = React2.useCallback(
2500
+ (touchX, touchY) => {
2501
+ if (!isDraggingRef.current || !onPositionChange) return;
2502
+ const deltaX = touchX - dragStartRef.current.touchX;
2503
+ const deltaY = touchY - dragStartRef.current.touchY;
2504
+ const newXPercent = dragStartRef.current.elementX + deltaX / containerWidth * 100;
2505
+ const newYPercent = dragStartRef.current.elementY + deltaY / containerHeight * 100;
2506
+ const constrained = constrainToViewport({
2507
+ newXPercent,
2508
+ newYPercent,
2509
+ elementSize,
2510
+ containerWidth,
2511
+ containerHeight
2512
+ });
2513
+ onPositionChange(constrained.x, constrained.y);
2514
+ },
2515
+ [onPositionChange, containerWidth, containerHeight, elementSize]
2516
+ );
2517
+ const handleDragEnd = React2.useCallback(() => {
2518
+ clearDragTimer();
2519
+ if (isDraggingRef.current) {
2520
+ isDraggingRef.current = false;
2521
+ setIsDragging(false);
2522
+ onDragEnd?.();
2523
+ }
2524
+ }, [clearDragTimer, onDragEnd]);
2525
+ return {
2526
+ isDragging,
2527
+ checkDragStart,
2528
+ handleDragMove,
2529
+ handleDragEnd,
2530
+ clearDragTimer,
2531
+ checkMoveThreshold
2532
+ };
2533
+ }
2534
+
2535
+ // src/components/VirtualController/hooks/useTouchHandlers.ts
2536
+ function useTouchHandlers({
2537
+ buttonType,
2538
+ isSystemButton,
2539
+ buttonSize,
2540
+ displayX,
2541
+ displayY,
2542
+ containerWidth,
2543
+ containerHeight,
2544
+ onPress,
2545
+ onPressDown,
2546
+ onRelease,
2547
+ onPositionChange
2548
+ }) {
2549
+ const isDraggingRef = React2.useRef(false);
2550
+ const drag = useDrag({
2551
+ elementSize: buttonSize,
2552
+ displayX,
2553
+ displayY,
2554
+ containerWidth,
2555
+ containerHeight,
2556
+ onPositionChange,
2557
+ centerThreshold: 0.4,
2558
+ onDragStart: () => {
2559
+ isDraggingRef.current = true;
2428
2560
  if (!isSystemButton) {
2429
2561
  onRelease(buttonType);
2430
2562
  }
2431
2563
  },
2432
- [displayX, displayY, containerWidth, containerHeight, isSystemButton, buttonType, onRelease]
2433
- );
2564
+ onDragEnd: () => {
2565
+ isDraggingRef.current = false;
2566
+ }
2567
+ });
2434
2568
  const handleTouchStart = React2.useCallback(
2435
2569
  (e) => {
2436
2570
  const touch = e.touches[0];
2437
- touchStartPosRef.current = { x: touch.clientX, y: touch.clientY };
2438
2571
  e.preventDefault();
2439
2572
  e.stopPropagation();
2440
2573
  if (navigator.vibrate) {
@@ -2449,57 +2582,32 @@ function useTouchHandlers({
2449
2582
  const target = e.currentTarget;
2450
2583
  if (!target) return;
2451
2584
  const rect = target.getBoundingClientRect();
2452
- const centerX = rect.left + rect.width / 2;
2453
- const centerY = rect.top + rect.height / 2;
2454
- const distance = Math.sqrt(
2455
- Math.pow(touch.clientX - centerX, 2) + Math.pow(touch.clientY - centerY, 2)
2456
- );
2457
- const dragThreshold = buttonSize * DRAG_CENTER_THRESHOLD;
2458
- if (distance < dragThreshold) {
2459
- dragTimerRef.current = setTimeout(() => {
2460
- if (!isDraggingRef.current) {
2461
- startDragging(touch.clientX, touch.clientY);
2462
- }
2463
- }, DRAG_HOLD_DELAY);
2464
- }
2585
+ drag.checkDragStart(touch.clientX, touch.clientY, rect);
2465
2586
  }
2466
2587
  },
2467
- [isSystemButton, buttonType, onPress, onPressDown, onPositionChange, buttonSize, startDragging]
2588
+ [isSystemButton, buttonType, onPress, onPressDown, onPositionChange, drag]
2468
2589
  );
2469
2590
  const handleTouchMove = React2.useCallback(
2470
2591
  (e) => {
2471
2592
  const touch = e.touches[0];
2472
2593
  if (onPositionChange && !isDraggingRef.current) {
2473
- const moveDistance = Math.sqrt(
2474
- Math.pow(touch.clientX - touchStartPosRef.current.x, 2) + Math.pow(touch.clientY - touchStartPosRef.current.y, 2)
2475
- );
2476
- if (moveDistance > DRAG_MOVE_THRESHOLD) {
2477
- clearDragTimer();
2478
- startDragging(touch.clientX, touch.clientY);
2479
- }
2594
+ drag.checkMoveThreshold(touch.clientX, touch.clientY);
2480
2595
  }
2481
- if (isDraggingRef.current && onPositionChange) {
2596
+ if (isDraggingRef.current) {
2482
2597
  e.preventDefault();
2483
2598
  e.stopPropagation();
2484
- const newX = touch.clientX - dragStartRef.current.x;
2485
- const newY = touch.clientY - dragStartRef.current.y;
2486
- const newXPercent = newX / containerWidth * 100;
2487
- const newYPercent = newY / containerHeight * 100;
2488
- const margin = buttonSize / 2 / Math.min(containerWidth, containerHeight) * 100;
2489
- const constrainedX = Math.max(margin, Math.min(100 - margin, newXPercent));
2490
- const constrainedY = Math.max(margin, Math.min(100 - margin, newYPercent));
2491
- onPositionChange(constrainedX, constrainedY);
2599
+ drag.handleDragMove(touch.clientX, touch.clientY);
2492
2600
  }
2493
2601
  },
2494
- [onPositionChange, clearDragTimer, startDragging, containerWidth, containerHeight, buttonSize]
2602
+ [onPositionChange, drag]
2495
2603
  );
2496
2604
  const handleTouchEnd = React2.useCallback(
2497
2605
  (e) => {
2498
- clearDragTimer();
2606
+ drag.clearDragTimer();
2499
2607
  if (isDraggingRef.current) {
2500
2608
  e.preventDefault();
2501
2609
  e.stopPropagation();
2502
- isDraggingRef.current = false;
2610
+ drag.handleDragEnd();
2503
2611
  return;
2504
2612
  }
2505
2613
  e.preventDefault();
@@ -2508,15 +2616,15 @@ function useTouchHandlers({
2508
2616
  onRelease(buttonType);
2509
2617
  }
2510
2618
  },
2511
- [clearDragTimer, isSystemButton, buttonType, onRelease]
2619
+ [drag, isSystemButton, buttonType, onRelease]
2512
2620
  );
2513
2621
  const handleTouchCancel = React2.useCallback(
2514
2622
  (e) => {
2515
- clearDragTimer();
2623
+ drag.clearDragTimer();
2516
2624
  if (isDraggingRef.current) {
2517
2625
  e.preventDefault();
2518
2626
  e.stopPropagation();
2519
- isDraggingRef.current = false;
2627
+ drag.handleDragEnd();
2520
2628
  return;
2521
2629
  }
2522
2630
  e.preventDefault();
@@ -2525,11 +2633,11 @@ function useTouchHandlers({
2525
2633
  onRelease(buttonType);
2526
2634
  }
2527
2635
  },
2528
- [clearDragTimer, isSystemButton, buttonType, onRelease]
2636
+ [drag, isSystemButton, buttonType, onRelease]
2529
2637
  );
2530
2638
  const cleanup = React2.useCallback(() => {
2531
- clearDragTimer();
2532
- }, [clearDragTimer]);
2639
+ drag.clearDragTimer();
2640
+ }, [drag]);
2533
2641
  return {
2534
2642
  handleTouchStart,
2535
2643
  handleTouchMove,
@@ -3385,7 +3493,6 @@ function dispatchKeyboardEvent(type, code) {
3385
3493
  canvas.dispatchEvent(event);
3386
3494
  return true;
3387
3495
  }
3388
- var DRAG_HOLD_DELAY2 = 350;
3389
3496
  var CENTER_TOUCH_RADIUS = 0.25;
3390
3497
  var Dpad = React2__default.default.memo(function Dpad2({
3391
3498
  size = 180,
@@ -3402,10 +3509,6 @@ var Dpad = React2__default.default.memo(function Dpad2({
3402
3509
  const dpadRef = React2.useRef(null);
3403
3510
  const activeTouchRef = React2.useRef(null);
3404
3511
  const activeDirectionsRef = React2.useRef(/* @__PURE__ */ new Set());
3405
- const [isDragging, setIsDragging] = React2.useState(false);
3406
- const dragTimerRef = React2.useRef(null);
3407
- const dragStartRef = React2.useRef({ x: 0, y: 0, touchX: 0, touchY: 0 });
3408
- const touchStartPosRef = React2.useRef({ x: 0, y: 0, time: 0 });
3409
3512
  const upPathRef = React2.useRef(null);
3410
3513
  const downPathRef = React2.useRef(null);
3411
3514
  const leftPathRef = React2.useRef(null);
@@ -3413,6 +3516,13 @@ var Dpad = React2__default.default.memo(function Dpad2({
3413
3516
  const centerCircleRef = React2.useRef(null);
3414
3517
  const displayX = customPosition ? customPosition.x : x;
3415
3518
  const displayY = customPosition ? customPosition.y : y;
3519
+ const releaseAllDirections = React2.useCallback((getKeyCode2) => {
3520
+ activeDirectionsRef.current.forEach((dir) => {
3521
+ const keyCode = getKeyCode2(dir);
3522
+ if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
3523
+ });
3524
+ activeDirectionsRef.current = /* @__PURE__ */ new Set();
3525
+ }, []);
3416
3526
  const getKeyCode = React2.useCallback((direction) => {
3417
3527
  if (!controls) {
3418
3528
  const defaults = {
@@ -3425,6 +3535,19 @@ var Dpad = React2__default.default.memo(function Dpad2({
3425
3535
  }
3426
3536
  return controls[direction] || "";
3427
3537
  }, [controls]);
3538
+ const drag = useDrag({
3539
+ elementSize: size,
3540
+ displayX,
3541
+ displayY,
3542
+ containerWidth,
3543
+ containerHeight,
3544
+ onPositionChange,
3545
+ centerThreshold: CENTER_TOUCH_RADIUS,
3546
+ onDragStart: () => {
3547
+ releaseAllDirections(getKeyCode);
3548
+ updateVisuals(/* @__PURE__ */ new Set());
3549
+ }
3550
+ });
3428
3551
  const getDirectionsFromTouch = React2.useCallback((touchX, touchY, rect) => {
3429
3552
  const centerX = rect.left + rect.width / 2;
3430
3553
  const centerY = rect.top + rect.height / 2;
@@ -3486,51 +3609,20 @@ var Dpad = React2__default.default.memo(function Dpad2({
3486
3609
  activeDirectionsRef.current = newDirections;
3487
3610
  updateVisuals(newDirections);
3488
3611
  }, [getKeyCode, updateVisuals]);
3489
- const clearDragTimer = React2.useCallback(() => {
3490
- if (dragTimerRef.current) {
3491
- clearTimeout(dragTimerRef.current);
3492
- dragTimerRef.current = null;
3493
- }
3494
- }, []);
3495
- const startDragging = React2.useCallback((touchX, touchY) => {
3496
- setIsDragging(true);
3497
- dragStartRef.current = {
3498
- x: displayX,
3499
- y: displayY,
3500
- touchX,
3501
- touchY
3502
- };
3503
- activeDirectionsRef.current.forEach((dir) => {
3504
- const keyCode = getKeyCode(dir);
3505
- if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
3506
- });
3507
- activeDirectionsRef.current = /* @__PURE__ */ new Set();
3508
- updateVisuals(/* @__PURE__ */ new Set());
3509
- if (navigator.vibrate) navigator.vibrate([10, 30, 10]);
3510
- }, [displayX, displayY, getKeyCode, updateVisuals]);
3511
3612
  const handleTouchStart = React2.useCallback((e) => {
3512
3613
  e.preventDefault();
3513
3614
  if (activeTouchRef.current !== null) return;
3514
3615
  const touch = e.changedTouches[0];
3515
3616
  activeTouchRef.current = touch.identifier;
3516
- touchStartPosRef.current = { x: touch.clientX, y: touch.clientY, time: Date.now() };
3517
3617
  const rect = dpadRef.current?.getBoundingClientRect();
3518
3618
  if (!rect) return;
3519
- const centerX = rect.left + rect.width / 2;
3520
- const centerY = rect.top + rect.height / 2;
3521
- const distFromCenter = Math.sqrt(
3522
- Math.pow(touch.clientX - centerX, 2) + Math.pow(touch.clientY - centerY, 2)
3523
- );
3524
- const centerRadius = size * CENTER_TOUCH_RADIUS;
3525
- if (distFromCenter < centerRadius && onPositionChange) {
3526
- dragTimerRef.current = setTimeout(() => {
3527
- startDragging(touch.clientX, touch.clientY);
3528
- }, DRAG_HOLD_DELAY2);
3619
+ if (onPositionChange) {
3620
+ drag.checkDragStart(touch.clientX, touch.clientY, rect);
3529
3621
  }
3530
- if (!isDragging) {
3622
+ if (!drag.isDragging) {
3531
3623
  updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3532
3624
  }
3533
- }, [getDirectionsFromTouch, updateDirections, isDragging, size, onPositionChange, startDragging]);
3625
+ }, [getDirectionsFromTouch, updateDirections, onPositionChange, drag]);
3534
3626
  const handleTouchMove = React2.useCallback((e) => {
3535
3627
  e.preventDefault();
3536
3628
  let touch = null;
@@ -3541,31 +3633,19 @@ var Dpad = React2__default.default.memo(function Dpad2({
3541
3633
  }
3542
3634
  }
3543
3635
  if (!touch) return;
3544
- if (isDragging && onPositionChange) {
3545
- const deltaX = touch.clientX - dragStartRef.current.touchX;
3546
- const deltaY = touch.clientY - dragStartRef.current.touchY;
3547
- const newXPercent = dragStartRef.current.x + deltaX / containerWidth * 100;
3548
- const newYPercent = dragStartRef.current.y + deltaY / containerHeight * 100;
3549
- const margin = size / 2 / Math.min(containerWidth, containerHeight) * 100;
3550
- const constrainedX = Math.max(margin, Math.min(100 - margin, newXPercent));
3551
- const constrainedY = Math.max(margin, Math.min(100 - margin, newYPercent));
3552
- onPositionChange(constrainedX, constrainedY);
3636
+ if (drag.isDragging) {
3637
+ drag.handleDragMove(touch.clientX, touch.clientY);
3553
3638
  } else {
3554
- const moveDistance = Math.sqrt(
3555
- Math.pow(touch.clientX - touchStartPosRef.current.x, 2) + Math.pow(touch.clientY - touchStartPosRef.current.y, 2)
3556
- );
3557
- if (moveDistance > 15) {
3558
- clearDragTimer();
3559
- }
3560
3639
  const rect = dpadRef.current?.getBoundingClientRect();
3561
3640
  if (rect) {
3641
+ drag.clearDragTimer();
3562
3642
  updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3563
3643
  }
3564
3644
  }
3565
- }, [isDragging, onPositionChange, containerWidth, containerHeight, size, getDirectionsFromTouch, updateDirections, clearDragTimer]);
3645
+ }, [drag, getDirectionsFromTouch, updateDirections]);
3566
3646
  const handleTouchEnd = React2.useCallback((e) => {
3567
3647
  e.preventDefault();
3568
- clearDragTimer();
3648
+ drag.clearDragTimer();
3569
3649
  let touchEnded = false;
3570
3650
  for (let i = 0; i < e.changedTouches.length; i++) {
3571
3651
  if (e.changedTouches[i].identifier === activeTouchRef.current) {
@@ -3575,8 +3655,8 @@ var Dpad = React2__default.default.memo(function Dpad2({
3575
3655
  }
3576
3656
  if (touchEnded) {
3577
3657
  activeTouchRef.current = null;
3578
- if (isDragging) {
3579
- setIsDragging(false);
3658
+ if (drag.isDragging) {
3659
+ drag.handleDragEnd();
3580
3660
  } else {
3581
3661
  activeDirectionsRef.current.forEach((dir) => {
3582
3662
  const keyCode = getKeyCode(dir);
@@ -3586,7 +3666,7 @@ var Dpad = React2__default.default.memo(function Dpad2({
3586
3666
  updateVisuals(/* @__PURE__ */ new Set());
3587
3667
  }
3588
3668
  }
3589
- }, [getKeyCode, updateVisuals, isDragging, clearDragTimer]);
3669
+ }, [getKeyCode, updateVisuals, drag]);
3590
3670
  React2.useEffect(() => {
3591
3671
  const dpad = dpadRef.current;
3592
3672
  if (!dpad) return;
@@ -3599,9 +3679,9 @@ var Dpad = React2__default.default.memo(function Dpad2({
3599
3679
  dpad.removeEventListener("touchmove", handleTouchMove);
3600
3680
  dpad.removeEventListener("touchend", handleTouchEnd);
3601
3681
  dpad.removeEventListener("touchcancel", handleTouchEnd);
3602
- clearDragTimer();
3682
+ drag.clearDragTimer();
3603
3683
  };
3604
- }, [handleTouchStart, handleTouchMove, handleTouchEnd, clearDragTimer]);
3684
+ }, [handleTouchStart, handleTouchMove, handleTouchEnd, drag]);
3605
3685
  const leftPx = displayX / 100 * containerWidth - size / 2;
3606
3686
  const topPx = displayY / 100 * containerHeight - size / 2;
3607
3687
  const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
@@ -3612,21 +3692,21 @@ var Dpad = React2__default.default.memo(function Dpad2({
3612
3692
  "div",
3613
3693
  {
3614
3694
  ref: dpadRef,
3615
- className: `absolute pointer-events-auto touch-manipulation select-none ${isDragging ? "opacity-60" : ""}`,
3695
+ className: `absolute pointer-events-auto touch-manipulation select-none ${drag.isDragging ? "opacity-60" : ""}`,
3616
3696
  style: {
3617
3697
  top: 0,
3618
3698
  left: 0,
3619
- transform: `translate3d(${leftPx}px, ${topPx}px, 0)${isDragging ? " scale(1.05)" : ""}`,
3699
+ transform: `translate3d(${leftPx}px, ${topPx}px, 0)${drag.isDragging ? " scale(1.05)" : ""}`,
3620
3700
  width: size,
3621
3701
  height: size,
3622
3702
  opacity: isLandscape ? 0.75 : 0.9,
3623
3703
  WebkitTouchCallout: "none",
3624
3704
  WebkitUserSelect: "none",
3625
3705
  touchAction: "none",
3626
- transition: isDragging ? "none" : "transform 0.1s ease-out"
3706
+ transition: drag.isDragging ? "none" : "transform 0.1s ease-out"
3627
3707
  },
3628
3708
  children: [
3629
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: `absolute inset-0 rounded-full bg-black/40 backdrop-blur-md border shadow-lg ${isDragging ? "border-white/50 ring-2 ring-white/30" : "border-white/10"}` }),
3709
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: `absolute inset-0 rounded-full bg-black/40 backdrop-blur-md border shadow-lg ${drag.isDragging ? "border-white/50 ring-2 ring-white/30" : "border-white/10"}` }),
3630
3710
  /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 100 100", className: "drop-shadow-xl relative z-10", children: [
3631
3711
  /* @__PURE__ */ jsxRuntime.jsx("path", { ref: upPathRef, d: dUp, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
3632
3712
  /* @__PURE__ */ jsxRuntime.jsx("path", { ref: rightPathRef, d: dRight, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
@@ -3639,9 +3719,9 @@ var Dpad = React2__default.default.memo(function Dpad2({
3639
3719
  cx: "50",
3640
3720
  cy: "50",
3641
3721
  r: "12",
3642
- fill: isDragging ? systemColor : "rgba(0,0,0,0.5)",
3643
- stroke: isDragging ? "#fff" : "rgba(255,255,255,0.3)",
3644
- strokeWidth: isDragging ? 2 : 1
3722
+ fill: drag.isDragging ? systemColor : "rgba(0,0,0,0.5)",
3723
+ stroke: drag.isDragging ? "#fff" : "rgba(255,255,255,0.3)",
3724
+ strokeWidth: drag.isDragging ? 2 : 1
3645
3725
  }
3646
3726
  ),
3647
3727
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 50,15 L 50,25 M 45,20 L 50,15 L 55,20", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", opacity: "0.8", pointerEvents: "none" }),
@@ -4069,6 +4149,45 @@ function FloatingFullscreenButton({ onClick, disabled = false }) {
4069
4149
  }
4070
4150
  );
4071
4151
  }
4152
+ function FloatingPauseButton({
4153
+ isPaused,
4154
+ onClick,
4155
+ disabled = false,
4156
+ systemColor = "#00FF41"
4157
+ }) {
4158
+ return /* @__PURE__ */ jsxRuntime.jsx(
4159
+ "button",
4160
+ {
4161
+ onClick,
4162
+ disabled,
4163
+ className: `
4164
+ fixed top-3 left-3 z-50
4165
+ px-3 py-2 rounded-xl
4166
+ bg-black/80 backdrop-blur-md
4167
+ border-2
4168
+ shadow-xl
4169
+ flex items-center gap-2
4170
+ transition-all duration-300
4171
+ hover:scale-105
4172
+ active:scale-95
4173
+ disabled:opacity-40 disabled:cursor-not-allowed
4174
+ touch-manipulation
4175
+ `,
4176
+ style: {
4177
+ paddingTop: "max(env(safe-area-inset-top, 0px), 8px)",
4178
+ borderColor: isPaused ? systemColor : "rgba(255,255,255,0.3)"
4179
+ },
4180
+ "aria-label": isPaused ? "Resume game" : "Pause game",
4181
+ children: isPaused ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4182
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Play, { size: 16, style: { color: systemColor }, fill: systemColor }),
4183
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-white text-xs font-bold uppercase tracking-wider", children: "Play" })
4184
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4185
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pause, { size: 16, className: "text-white/80" }),
4186
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-white text-xs font-bold uppercase tracking-wider", children: "Pause" })
4187
+ ] })
4188
+ }
4189
+ );
4190
+ }
4072
4191
  function LoadingSpinner({ color, size = "lg" }) {
4073
4192
  const sizeClass = size === "lg" ? "w-12 h-12" : "w-8 h-8";
4074
4193
  return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: `${sizeClass} animate-spin`, style: { color } });
@@ -9202,6 +9321,14 @@ var GamePlayerInner = React2.memo(function GamePlayerInner2(props) {
9202
9321
  disabled: status === "loading" || status === "error"
9203
9322
  }
9204
9323
  ),
9324
+ isFullscreen2 && isMobile && (status === "running" || status === "paused") && /* @__PURE__ */ jsxRuntime.jsx(
9325
+ FloatingPauseButton,
9326
+ {
9327
+ isPaused,
9328
+ onClick: handlePauseToggle,
9329
+ systemColor
9330
+ }
9331
+ ),
9205
9332
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute top-2 right-2 z-40 flex flex-col items-end gap-2 pointer-events-auto", children: [
9206
9333
  /* @__PURE__ */ jsxRuntime.jsx(
9207
9334
  RecordingIndicator_default,