import React from 'react'; /** * Manifold VirtualJoystick — circular control. Drag the glowing orange knob; * motion is constrained to the circle. Emits normalized (x, y) in [0,1], y-up. */ export function VirtualJoystick({ size = 200, position, onMove, onGrab, onRelease, disabled = false, ariaLabel = 'virtual joystick', style }) { const [internal, setInternal] = React.useState([0.5, 0.5]); const [dragging, setDragging] = React.useState(false); const ref = React.useRef(null); const pos = position || internal; const update = (e) => { const el = ref.current; if (!el) return; const r = el.getBoundingClientRect(); let x = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)); let y = Math.max(0, Math.min(1, 1 - (e.clientY - r.top) / r.height)); const dx = x - 0.5, dy = y - 0.5; const dist = Math.sqrt(dx * dx + dy * dy); if (dist > 0.5 && dist > 1e-12) { x = 0.5 + (dx / dist) * 0.5; y = 0.5 + (dy / dist) * 0.5; } if (!position) setInternal([x, y]); onMove && onMove(x, y); }; const down = (e) => { if (disabled) return; e.currentTarget.setPointerCapture?.(e.pointerId); setDragging(true); onGrab && onGrab(); update(e); }; const move = (e) => { if (dragging) update(e); }; const up = (e) => { if (!dragging) return; e.currentTarget.releasePointerCapture?.(e.pointerId); setDragging(false); onRelease && onRelease(); }; const [x, y] = pos; return (