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.mjs CHANGED
@@ -2276,7 +2276,8 @@ var SIX_BUTTON_LAYOUT = {
2276
2276
  { type: "y", label: "A", x: 72, y: 64, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2277
2277
  { type: "b", label: "B", x: 82, y: 60, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2278
2278
  { type: "a", label: "C", x: 92, y: 56, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2279
- { type: "start", label: "START", x: 50, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2279
+ { type: "select", label: "SELECT", x: SELECT_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" },
2280
+ { type: "start", label: "START", x: START_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2280
2281
  ]
2281
2282
  };
2282
2283
  var SATURN_LAYOUT = {
@@ -2295,7 +2296,8 @@ var SATURN_LAYOUT = {
2295
2296
  // Triggers (L2/R2 for Saturn L/R)
2296
2297
  { type: "l2", label: "L", x: 8, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2297
2298
  { type: "r2", label: "R", x: 92, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2298
- { type: "start", label: "START", x: 50, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2299
+ { type: "select", label: "SELECT", x: SELECT_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" },
2300
+ { type: "start", label: "START", x: START_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2299
2301
  ]
2300
2302
  };
2301
2303
  var NEOGEO_LAYOUT = {
@@ -2344,9 +2346,10 @@ var N64_LAYOUT = {
2344
2346
  // Shoulders
2345
2347
  { type: "l", label: "L", x: 8, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2346
2348
  { type: "r", label: "R", x: 92, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2347
- // Z trigger (use select as workaround since it's a unique button)
2348
- { type: "select", label: "Z", x: 8, y: 35, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2349
- { type: "start", label: "START", x: 50, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2349
+ // Z trigger (use l3 button for Z trigger)
2350
+ { type: "l3", label: "Z", x: 8, y: 35, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2351
+ { type: "select", label: "SELECT", x: SELECT_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" },
2352
+ { type: "start", label: "START", x: START_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2350
2353
  ]
2351
2354
  };
2352
2355
  var DREAMCAST_LAYOUT = {
@@ -2361,7 +2364,8 @@ var DREAMCAST_LAYOUT = {
2361
2364
  // Triggers
2362
2365
  { type: "l", label: "L", x: 8, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2363
2366
  { type: "r", label: "R", x: 92, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2364
- { type: "start", label: "START", x: 50, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2367
+ { type: "select", label: "SELECT", x: SELECT_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" },
2368
+ { type: "start", label: "START", x: START_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2365
2369
  ]
2366
2370
  };
2367
2371
  function getLayoutForSystem(system) {
@@ -2383,26 +2387,49 @@ function getLayoutForSystem(system) {
2383
2387
  if (s.includes("ATARI")) return TWO_BUTTON_LAYOUT;
2384
2388
  return TWO_BUTTON_LAYOUT;
2385
2389
  }
2386
- var DRAG_HOLD_DELAY = 350;
2390
+
2391
+ // src/components/VirtualController/utils/dragConstraints.ts
2392
+ function constrainToViewport({
2393
+ newXPercent,
2394
+ newYPercent,
2395
+ elementSize,
2396
+ containerWidth,
2397
+ containerHeight
2398
+ }) {
2399
+ const xMargin = elementSize / 2 / containerWidth * 100;
2400
+ const yMargin = elementSize / 2 / containerHeight * 100;
2401
+ return {
2402
+ x: Math.max(xMargin, Math.min(100 - xMargin, newXPercent)),
2403
+ y: Math.max(yMargin, Math.min(100 - yMargin, newYPercent))
2404
+ };
2405
+ }
2406
+
2407
+ // src/components/VirtualController/hooks/useDrag.ts
2408
+ var DEFAULT_HOLD_DELAY = 350;
2409
+ var DEFAULT_CENTER_THRESHOLD = 0.4;
2387
2410
  var DRAG_MOVE_THRESHOLD = 10;
2388
- var DRAG_CENTER_THRESHOLD = 0.4;
2389
- function useTouchHandlers({
2390
- buttonType,
2391
- isSystemButton,
2392
- buttonSize,
2411
+ function useDrag({
2412
+ elementSize,
2393
2413
  displayX,
2394
2414
  displayY,
2395
2415
  containerWidth,
2396
2416
  containerHeight,
2397
- onPress,
2398
- onPressDown,
2399
- onRelease,
2400
- onPositionChange
2417
+ onPositionChange,
2418
+ holdDelay = DEFAULT_HOLD_DELAY,
2419
+ centerThreshold = DEFAULT_CENTER_THRESHOLD,
2420
+ onDragStart,
2421
+ onDragEnd
2401
2422
  }) {
2423
+ const [isDragging, setIsDragging] = useState(false);
2402
2424
  const isDraggingRef = useRef(false);
2403
- const dragStartRef = useRef({ x: 0, y: 0 });
2404
2425
  const dragTimerRef = useRef(null);
2405
2426
  const touchStartPosRef = useRef({ x: 0, y: 0 });
2427
+ const dragStartRef = useRef({
2428
+ elementX: 0,
2429
+ elementY: 0,
2430
+ touchX: 0,
2431
+ touchY: 0
2432
+ });
2406
2433
  const clearDragTimer = useCallback(() => {
2407
2434
  if (dragTimerRef.current) {
2408
2435
  clearTimeout(dragTimerRef.current);
@@ -2412,23 +2439,129 @@ function useTouchHandlers({
2412
2439
  const startDragging = useCallback(
2413
2440
  (touchX, touchY) => {
2414
2441
  isDraggingRef.current = true;
2442
+ setIsDragging(true);
2415
2443
  dragStartRef.current = {
2416
- x: touchX - displayX / 100 * containerWidth,
2417
- y: touchY - displayY / 100 * containerHeight
2444
+ elementX: displayX,
2445
+ elementY: displayY,
2446
+ touchX,
2447
+ touchY
2418
2448
  };
2419
2449
  if (navigator.vibrate) {
2420
2450
  navigator.vibrate([10, 30, 10]);
2421
2451
  }
2452
+ onDragStart?.();
2453
+ },
2454
+ [displayX, displayY, onDragStart]
2455
+ );
2456
+ const checkDragStart = useCallback(
2457
+ (touchX, touchY, elementRect) => {
2458
+ if (!onPositionChange) return false;
2459
+ touchStartPosRef.current = { x: touchX, y: touchY };
2460
+ const centerX = elementRect.left + elementRect.width / 2;
2461
+ const centerY = elementRect.top + elementRect.height / 2;
2462
+ const distFromCenter = Math.sqrt(
2463
+ Math.pow(touchX - centerX, 2) + Math.pow(touchY - centerY, 2)
2464
+ );
2465
+ const centerRadius = elementSize * centerThreshold;
2466
+ if (distFromCenter < centerRadius) {
2467
+ dragTimerRef.current = setTimeout(() => {
2468
+ if (!isDraggingRef.current) {
2469
+ startDragging(touchX, touchY);
2470
+ }
2471
+ }, holdDelay);
2472
+ return true;
2473
+ }
2474
+ return false;
2475
+ },
2476
+ [onPositionChange, elementSize, centerThreshold, holdDelay, startDragging]
2477
+ );
2478
+ const checkMoveThreshold = useCallback(
2479
+ (touchX, touchY) => {
2480
+ if (!onPositionChange || isDraggingRef.current) return false;
2481
+ const moveDistance = Math.sqrt(
2482
+ Math.pow(touchX - touchStartPosRef.current.x, 2) + Math.pow(touchY - touchStartPosRef.current.y, 2)
2483
+ );
2484
+ if (moveDistance > DRAG_MOVE_THRESHOLD) {
2485
+ clearDragTimer();
2486
+ startDragging(touchX, touchY);
2487
+ return true;
2488
+ }
2489
+ return false;
2490
+ },
2491
+ [onPositionChange, clearDragTimer, startDragging]
2492
+ );
2493
+ const handleDragMove = useCallback(
2494
+ (touchX, touchY) => {
2495
+ if (!isDraggingRef.current || !onPositionChange) return;
2496
+ const deltaX = touchX - dragStartRef.current.touchX;
2497
+ const deltaY = touchY - dragStartRef.current.touchY;
2498
+ const newXPercent = dragStartRef.current.elementX + deltaX / containerWidth * 100;
2499
+ const newYPercent = dragStartRef.current.elementY + deltaY / containerHeight * 100;
2500
+ const constrained = constrainToViewport({
2501
+ newXPercent,
2502
+ newYPercent,
2503
+ elementSize,
2504
+ containerWidth,
2505
+ containerHeight
2506
+ });
2507
+ onPositionChange(constrained.x, constrained.y);
2508
+ },
2509
+ [onPositionChange, containerWidth, containerHeight, elementSize]
2510
+ );
2511
+ const handleDragEnd = useCallback(() => {
2512
+ clearDragTimer();
2513
+ if (isDraggingRef.current) {
2514
+ isDraggingRef.current = false;
2515
+ setIsDragging(false);
2516
+ onDragEnd?.();
2517
+ }
2518
+ }, [clearDragTimer, onDragEnd]);
2519
+ return {
2520
+ isDragging,
2521
+ checkDragStart,
2522
+ handleDragMove,
2523
+ handleDragEnd,
2524
+ clearDragTimer,
2525
+ checkMoveThreshold
2526
+ };
2527
+ }
2528
+
2529
+ // src/components/VirtualController/hooks/useTouchHandlers.ts
2530
+ function useTouchHandlers({
2531
+ buttonType,
2532
+ isSystemButton,
2533
+ buttonSize,
2534
+ displayX,
2535
+ displayY,
2536
+ containerWidth,
2537
+ containerHeight,
2538
+ onPress,
2539
+ onPressDown,
2540
+ onRelease,
2541
+ onPositionChange
2542
+ }) {
2543
+ const isDraggingRef = useRef(false);
2544
+ const drag = useDrag({
2545
+ elementSize: buttonSize,
2546
+ displayX,
2547
+ displayY,
2548
+ containerWidth,
2549
+ containerHeight,
2550
+ onPositionChange,
2551
+ centerThreshold: 0.4,
2552
+ onDragStart: () => {
2553
+ isDraggingRef.current = true;
2422
2554
  if (!isSystemButton) {
2423
2555
  onRelease(buttonType);
2424
2556
  }
2425
2557
  },
2426
- [displayX, displayY, containerWidth, containerHeight, isSystemButton, buttonType, onRelease]
2427
- );
2558
+ onDragEnd: () => {
2559
+ isDraggingRef.current = false;
2560
+ }
2561
+ });
2428
2562
  const handleTouchStart = useCallback(
2429
2563
  (e) => {
2430
2564
  const touch = e.touches[0];
2431
- touchStartPosRef.current = { x: touch.clientX, y: touch.clientY };
2432
2565
  e.preventDefault();
2433
2566
  e.stopPropagation();
2434
2567
  if (navigator.vibrate) {
@@ -2443,57 +2576,32 @@ function useTouchHandlers({
2443
2576
  const target = e.currentTarget;
2444
2577
  if (!target) return;
2445
2578
  const rect = target.getBoundingClientRect();
2446
- const centerX = rect.left + rect.width / 2;
2447
- const centerY = rect.top + rect.height / 2;
2448
- const distance = Math.sqrt(
2449
- Math.pow(touch.clientX - centerX, 2) + Math.pow(touch.clientY - centerY, 2)
2450
- );
2451
- const dragThreshold = buttonSize * DRAG_CENTER_THRESHOLD;
2452
- if (distance < dragThreshold) {
2453
- dragTimerRef.current = setTimeout(() => {
2454
- if (!isDraggingRef.current) {
2455
- startDragging(touch.clientX, touch.clientY);
2456
- }
2457
- }, DRAG_HOLD_DELAY);
2458
- }
2579
+ drag.checkDragStart(touch.clientX, touch.clientY, rect);
2459
2580
  }
2460
2581
  },
2461
- [isSystemButton, buttonType, onPress, onPressDown, onPositionChange, buttonSize, startDragging]
2582
+ [isSystemButton, buttonType, onPress, onPressDown, onPositionChange, drag]
2462
2583
  );
2463
2584
  const handleTouchMove = useCallback(
2464
2585
  (e) => {
2465
2586
  const touch = e.touches[0];
2466
2587
  if (onPositionChange && !isDraggingRef.current) {
2467
- const moveDistance = Math.sqrt(
2468
- Math.pow(touch.clientX - touchStartPosRef.current.x, 2) + Math.pow(touch.clientY - touchStartPosRef.current.y, 2)
2469
- );
2470
- if (moveDistance > DRAG_MOVE_THRESHOLD) {
2471
- clearDragTimer();
2472
- startDragging(touch.clientX, touch.clientY);
2473
- }
2588
+ drag.checkMoveThreshold(touch.clientX, touch.clientY);
2474
2589
  }
2475
- if (isDraggingRef.current && onPositionChange) {
2590
+ if (isDraggingRef.current) {
2476
2591
  e.preventDefault();
2477
2592
  e.stopPropagation();
2478
- const newX = touch.clientX - dragStartRef.current.x;
2479
- const newY = touch.clientY - dragStartRef.current.y;
2480
- const newXPercent = newX / containerWidth * 100;
2481
- const newYPercent = newY / containerHeight * 100;
2482
- const margin = buttonSize / 2 / Math.min(containerWidth, containerHeight) * 100;
2483
- const constrainedX = Math.max(margin, Math.min(100 - margin, newXPercent));
2484
- const constrainedY = Math.max(margin, Math.min(100 - margin, newYPercent));
2485
- onPositionChange(constrainedX, constrainedY);
2593
+ drag.handleDragMove(touch.clientX, touch.clientY);
2486
2594
  }
2487
2595
  },
2488
- [onPositionChange, clearDragTimer, startDragging, containerWidth, containerHeight, buttonSize]
2596
+ [onPositionChange, drag]
2489
2597
  );
2490
2598
  const handleTouchEnd = useCallback(
2491
2599
  (e) => {
2492
- clearDragTimer();
2600
+ drag.clearDragTimer();
2493
2601
  if (isDraggingRef.current) {
2494
2602
  e.preventDefault();
2495
2603
  e.stopPropagation();
2496
- isDraggingRef.current = false;
2604
+ drag.handleDragEnd();
2497
2605
  return;
2498
2606
  }
2499
2607
  e.preventDefault();
@@ -2502,15 +2610,15 @@ function useTouchHandlers({
2502
2610
  onRelease(buttonType);
2503
2611
  }
2504
2612
  },
2505
- [clearDragTimer, isSystemButton, buttonType, onRelease]
2613
+ [drag, isSystemButton, buttonType, onRelease]
2506
2614
  );
2507
2615
  const handleTouchCancel = useCallback(
2508
2616
  (e) => {
2509
- clearDragTimer();
2617
+ drag.clearDragTimer();
2510
2618
  if (isDraggingRef.current) {
2511
2619
  e.preventDefault();
2512
2620
  e.stopPropagation();
2513
- isDraggingRef.current = false;
2621
+ drag.handleDragEnd();
2514
2622
  return;
2515
2623
  }
2516
2624
  e.preventDefault();
@@ -2519,11 +2627,11 @@ function useTouchHandlers({
2519
2627
  onRelease(buttonType);
2520
2628
  }
2521
2629
  },
2522
- [clearDragTimer, isSystemButton, buttonType, onRelease]
2630
+ [drag, isSystemButton, buttonType, onRelease]
2523
2631
  );
2524
2632
  const cleanup = useCallback(() => {
2525
- clearDragTimer();
2526
- }, [clearDragTimer]);
2633
+ drag.clearDragTimer();
2634
+ }, [drag]);
2527
2635
  return {
2528
2636
  handleTouchStart,
2529
2637
  handleTouchMove,
@@ -3379,7 +3487,6 @@ function dispatchKeyboardEvent(type, code) {
3379
3487
  canvas.dispatchEvent(event);
3380
3488
  return true;
3381
3489
  }
3382
- var DRAG_HOLD_DELAY2 = 350;
3383
3490
  var CENTER_TOUCH_RADIUS = 0.25;
3384
3491
  var Dpad = React2.memo(function Dpad2({
3385
3492
  size = 180,
@@ -3396,10 +3503,6 @@ var Dpad = React2.memo(function Dpad2({
3396
3503
  const dpadRef = useRef(null);
3397
3504
  const activeTouchRef = useRef(null);
3398
3505
  const activeDirectionsRef = useRef(/* @__PURE__ */ new Set());
3399
- const [isDragging, setIsDragging] = useState(false);
3400
- const dragTimerRef = useRef(null);
3401
- const dragStartRef = useRef({ x: 0, y: 0, touchX: 0, touchY: 0 });
3402
- const touchStartPosRef = useRef({ x: 0, y: 0, time: 0 });
3403
3506
  const upPathRef = useRef(null);
3404
3507
  const downPathRef = useRef(null);
3405
3508
  const leftPathRef = useRef(null);
@@ -3407,6 +3510,13 @@ var Dpad = React2.memo(function Dpad2({
3407
3510
  const centerCircleRef = useRef(null);
3408
3511
  const displayX = customPosition ? customPosition.x : x;
3409
3512
  const displayY = customPosition ? customPosition.y : y;
3513
+ const releaseAllDirections = useCallback((getKeyCode2) => {
3514
+ activeDirectionsRef.current.forEach((dir) => {
3515
+ const keyCode = getKeyCode2(dir);
3516
+ if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
3517
+ });
3518
+ activeDirectionsRef.current = /* @__PURE__ */ new Set();
3519
+ }, []);
3410
3520
  const getKeyCode = useCallback((direction) => {
3411
3521
  if (!controls) {
3412
3522
  const defaults = {
@@ -3419,6 +3529,19 @@ var Dpad = React2.memo(function Dpad2({
3419
3529
  }
3420
3530
  return controls[direction] || "";
3421
3531
  }, [controls]);
3532
+ const drag = useDrag({
3533
+ elementSize: size,
3534
+ displayX,
3535
+ displayY,
3536
+ containerWidth,
3537
+ containerHeight,
3538
+ onPositionChange,
3539
+ centerThreshold: CENTER_TOUCH_RADIUS,
3540
+ onDragStart: () => {
3541
+ releaseAllDirections(getKeyCode);
3542
+ updateVisuals(/* @__PURE__ */ new Set());
3543
+ }
3544
+ });
3422
3545
  const getDirectionsFromTouch = useCallback((touchX, touchY, rect) => {
3423
3546
  const centerX = rect.left + rect.width / 2;
3424
3547
  const centerY = rect.top + rect.height / 2;
@@ -3480,51 +3603,20 @@ var Dpad = React2.memo(function Dpad2({
3480
3603
  activeDirectionsRef.current = newDirections;
3481
3604
  updateVisuals(newDirections);
3482
3605
  }, [getKeyCode, updateVisuals]);
3483
- const clearDragTimer = useCallback(() => {
3484
- if (dragTimerRef.current) {
3485
- clearTimeout(dragTimerRef.current);
3486
- dragTimerRef.current = null;
3487
- }
3488
- }, []);
3489
- const startDragging = useCallback((touchX, touchY) => {
3490
- setIsDragging(true);
3491
- dragStartRef.current = {
3492
- x: displayX,
3493
- y: displayY,
3494
- touchX,
3495
- touchY
3496
- };
3497
- activeDirectionsRef.current.forEach((dir) => {
3498
- const keyCode = getKeyCode(dir);
3499
- if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
3500
- });
3501
- activeDirectionsRef.current = /* @__PURE__ */ new Set();
3502
- updateVisuals(/* @__PURE__ */ new Set());
3503
- if (navigator.vibrate) navigator.vibrate([10, 30, 10]);
3504
- }, [displayX, displayY, getKeyCode, updateVisuals]);
3505
3606
  const handleTouchStart = useCallback((e) => {
3506
3607
  e.preventDefault();
3507
3608
  if (activeTouchRef.current !== null) return;
3508
3609
  const touch = e.changedTouches[0];
3509
3610
  activeTouchRef.current = touch.identifier;
3510
- touchStartPosRef.current = { x: touch.clientX, y: touch.clientY, time: Date.now() };
3511
3611
  const rect = dpadRef.current?.getBoundingClientRect();
3512
3612
  if (!rect) return;
3513
- const centerX = rect.left + rect.width / 2;
3514
- const centerY = rect.top + rect.height / 2;
3515
- const distFromCenter = Math.sqrt(
3516
- Math.pow(touch.clientX - centerX, 2) + Math.pow(touch.clientY - centerY, 2)
3517
- );
3518
- const centerRadius = size * CENTER_TOUCH_RADIUS;
3519
- if (distFromCenter < centerRadius && onPositionChange) {
3520
- dragTimerRef.current = setTimeout(() => {
3521
- startDragging(touch.clientX, touch.clientY);
3522
- }, DRAG_HOLD_DELAY2);
3613
+ if (onPositionChange) {
3614
+ drag.checkDragStart(touch.clientX, touch.clientY, rect);
3523
3615
  }
3524
- if (!isDragging) {
3616
+ if (!drag.isDragging) {
3525
3617
  updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3526
3618
  }
3527
- }, [getDirectionsFromTouch, updateDirections, isDragging, size, onPositionChange, startDragging]);
3619
+ }, [getDirectionsFromTouch, updateDirections, onPositionChange, drag]);
3528
3620
  const handleTouchMove = useCallback((e) => {
3529
3621
  e.preventDefault();
3530
3622
  let touch = null;
@@ -3535,31 +3627,19 @@ var Dpad = React2.memo(function Dpad2({
3535
3627
  }
3536
3628
  }
3537
3629
  if (!touch) return;
3538
- if (isDragging && onPositionChange) {
3539
- const deltaX = touch.clientX - dragStartRef.current.touchX;
3540
- const deltaY = touch.clientY - dragStartRef.current.touchY;
3541
- const newXPercent = dragStartRef.current.x + deltaX / containerWidth * 100;
3542
- const newYPercent = dragStartRef.current.y + deltaY / containerHeight * 100;
3543
- const margin = size / 2 / Math.min(containerWidth, containerHeight) * 100;
3544
- const constrainedX = Math.max(margin, Math.min(100 - margin, newXPercent));
3545
- const constrainedY = Math.max(margin, Math.min(100 - margin, newYPercent));
3546
- onPositionChange(constrainedX, constrainedY);
3630
+ if (drag.isDragging) {
3631
+ drag.handleDragMove(touch.clientX, touch.clientY);
3547
3632
  } else {
3548
- const moveDistance = Math.sqrt(
3549
- Math.pow(touch.clientX - touchStartPosRef.current.x, 2) + Math.pow(touch.clientY - touchStartPosRef.current.y, 2)
3550
- );
3551
- if (moveDistance > 15) {
3552
- clearDragTimer();
3553
- }
3554
3633
  const rect = dpadRef.current?.getBoundingClientRect();
3555
3634
  if (rect) {
3635
+ drag.clearDragTimer();
3556
3636
  updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3557
3637
  }
3558
3638
  }
3559
- }, [isDragging, onPositionChange, containerWidth, containerHeight, size, getDirectionsFromTouch, updateDirections, clearDragTimer]);
3639
+ }, [drag, getDirectionsFromTouch, updateDirections]);
3560
3640
  const handleTouchEnd = useCallback((e) => {
3561
3641
  e.preventDefault();
3562
- clearDragTimer();
3642
+ drag.clearDragTimer();
3563
3643
  let touchEnded = false;
3564
3644
  for (let i = 0; i < e.changedTouches.length; i++) {
3565
3645
  if (e.changedTouches[i].identifier === activeTouchRef.current) {
@@ -3569,8 +3649,8 @@ var Dpad = React2.memo(function Dpad2({
3569
3649
  }
3570
3650
  if (touchEnded) {
3571
3651
  activeTouchRef.current = null;
3572
- if (isDragging) {
3573
- setIsDragging(false);
3652
+ if (drag.isDragging) {
3653
+ drag.handleDragEnd();
3574
3654
  } else {
3575
3655
  activeDirectionsRef.current.forEach((dir) => {
3576
3656
  const keyCode = getKeyCode(dir);
@@ -3580,7 +3660,7 @@ var Dpad = React2.memo(function Dpad2({
3580
3660
  updateVisuals(/* @__PURE__ */ new Set());
3581
3661
  }
3582
3662
  }
3583
- }, [getKeyCode, updateVisuals, isDragging, clearDragTimer]);
3663
+ }, [getKeyCode, updateVisuals, drag]);
3584
3664
  useEffect(() => {
3585
3665
  const dpad = dpadRef.current;
3586
3666
  if (!dpad) return;
@@ -3593,9 +3673,9 @@ var Dpad = React2.memo(function Dpad2({
3593
3673
  dpad.removeEventListener("touchmove", handleTouchMove);
3594
3674
  dpad.removeEventListener("touchend", handleTouchEnd);
3595
3675
  dpad.removeEventListener("touchcancel", handleTouchEnd);
3596
- clearDragTimer();
3676
+ drag.clearDragTimer();
3597
3677
  };
3598
- }, [handleTouchStart, handleTouchMove, handleTouchEnd, clearDragTimer]);
3678
+ }, [handleTouchStart, handleTouchMove, handleTouchEnd, drag]);
3599
3679
  const leftPx = displayX / 100 * containerWidth - size / 2;
3600
3680
  const topPx = displayY / 100 * containerHeight - size / 2;
3601
3681
  const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
@@ -3606,21 +3686,21 @@ var Dpad = React2.memo(function Dpad2({
3606
3686
  "div",
3607
3687
  {
3608
3688
  ref: dpadRef,
3609
- className: `absolute pointer-events-auto touch-manipulation select-none ${isDragging ? "opacity-60" : ""}`,
3689
+ className: `absolute pointer-events-auto touch-manipulation select-none ${drag.isDragging ? "opacity-60" : ""}`,
3610
3690
  style: {
3611
3691
  top: 0,
3612
3692
  left: 0,
3613
- transform: `translate3d(${leftPx}px, ${topPx}px, 0)${isDragging ? " scale(1.05)" : ""}`,
3693
+ transform: `translate3d(${leftPx}px, ${topPx}px, 0)${drag.isDragging ? " scale(1.05)" : ""}`,
3614
3694
  width: size,
3615
3695
  height: size,
3616
3696
  opacity: isLandscape ? 0.75 : 0.9,
3617
3697
  WebkitTouchCallout: "none",
3618
3698
  WebkitUserSelect: "none",
3619
3699
  touchAction: "none",
3620
- transition: isDragging ? "none" : "transform 0.1s ease-out"
3700
+ transition: drag.isDragging ? "none" : "transform 0.1s ease-out"
3621
3701
  },
3622
3702
  children: [
3623
- /* @__PURE__ */ 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"}` }),
3703
+ /* @__PURE__ */ 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"}` }),
3624
3704
  /* @__PURE__ */ jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 100 100", className: "drop-shadow-xl relative z-10", children: [
3625
3705
  /* @__PURE__ */ 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" }),
3626
3706
  /* @__PURE__ */ 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" }),
@@ -3633,9 +3713,9 @@ var Dpad = React2.memo(function Dpad2({
3633
3713
  cx: "50",
3634
3714
  cy: "50",
3635
3715
  r: "12",
3636
- fill: isDragging ? systemColor : "rgba(0,0,0,0.5)",
3637
- stroke: isDragging ? "#fff" : "rgba(255,255,255,0.3)",
3638
- strokeWidth: isDragging ? 2 : 1
3716
+ fill: drag.isDragging ? systemColor : "rgba(0,0,0,0.5)",
3717
+ stroke: drag.isDragging ? "#fff" : "rgba(255,255,255,0.3)",
3718
+ strokeWidth: drag.isDragging ? 2 : 1
3639
3719
  }
3640
3720
  ),
3641
3721
  /* @__PURE__ */ 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" }),
@@ -4063,6 +4143,45 @@ function FloatingFullscreenButton({ onClick, disabled = false }) {
4063
4143
  }
4064
4144
  );
4065
4145
  }
4146
+ function FloatingPauseButton({
4147
+ isPaused,
4148
+ onClick,
4149
+ disabled = false,
4150
+ systemColor = "#00FF41"
4151
+ }) {
4152
+ return /* @__PURE__ */ jsx(
4153
+ "button",
4154
+ {
4155
+ onClick,
4156
+ disabled,
4157
+ className: `
4158
+ fixed top-3 left-3 z-50
4159
+ px-3 py-2 rounded-xl
4160
+ bg-black/80 backdrop-blur-md
4161
+ border-2
4162
+ shadow-xl
4163
+ flex items-center gap-2
4164
+ transition-all duration-300
4165
+ hover:scale-105
4166
+ active:scale-95
4167
+ disabled:opacity-40 disabled:cursor-not-allowed
4168
+ touch-manipulation
4169
+ `,
4170
+ style: {
4171
+ paddingTop: "max(env(safe-area-inset-top, 0px), 8px)",
4172
+ borderColor: isPaused ? systemColor : "rgba(255,255,255,0.3)"
4173
+ },
4174
+ "aria-label": isPaused ? "Resume game" : "Pause game",
4175
+ children: isPaused ? /* @__PURE__ */ jsxs(Fragment, { children: [
4176
+ /* @__PURE__ */ jsx(Play, { size: 16, style: { color: systemColor }, fill: systemColor }),
4177
+ /* @__PURE__ */ jsx("span", { className: "text-white text-xs font-bold uppercase tracking-wider", children: "Play" })
4178
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
4179
+ /* @__PURE__ */ jsx(Pause, { size: 16, className: "text-white/80" }),
4180
+ /* @__PURE__ */ jsx("span", { className: "text-white text-xs font-bold uppercase tracking-wider", children: "Pause" })
4181
+ ] })
4182
+ }
4183
+ );
4184
+ }
4066
4185
  function LoadingSpinner({ color, size = "lg" }) {
4067
4186
  const sizeClass = size === "lg" ? "w-12 h-12" : "w-8 h-8";
4068
4187
  return /* @__PURE__ */ jsx(Loader2, { className: `${sizeClass} animate-spin`, style: { color } });
@@ -9196,6 +9315,14 @@ var GamePlayerInner = memo(function GamePlayerInner2(props) {
9196
9315
  disabled: status === "loading" || status === "error"
9197
9316
  }
9198
9317
  ),
9318
+ isFullscreen2 && isMobile && (status === "running" || status === "paused") && /* @__PURE__ */ jsx(
9319
+ FloatingPauseButton,
9320
+ {
9321
+ isPaused,
9322
+ onClick: handlePauseToggle,
9323
+ systemColor
9324
+ }
9325
+ ),
9199
9326
  /* @__PURE__ */ jsxs("div", { className: "absolute top-2 right-2 z-40 flex flex-col items-end gap-2 pointer-events-auto", children: [
9200
9327
  /* @__PURE__ */ jsx(
9201
9328
  RecordingIndicator_default,