"use client"; import * as React from "react"; import { cn } from "./lib/utils"; export interface JoystickProps extends Omit, "onMove"> { size?: number; onMove?: (x: number, y: number) => void; onRelease?: () => void; } /** Analog joystick: drag the knob inside a dashed base, reports normalized -1..1 axes. */ export function Joystick({ size = 132, onMove, onRelease, className, ...props }: JoystickProps) { const baseRef = React.useRef(null); const [knob, setKnob] = React.useState({ x: 0, y: 0 }); const [active, setActive] = React.useState(false); const radius = size / 2 - 24; const update = (clientX: number, clientY: number) => { const rect = baseRef.current?.getBoundingClientRect(); if (!rect) return; let dx = clientX - (rect.left + rect.width / 2); let dy = clientY - (rect.top + rect.height / 2); const dist = Math.hypot(dx, dy) || 1; const clamped = Math.min(dist, radius); dx = (dx / dist) * clamped; dy = (dy / dist) * clamped; setKnob({ x: dx, y: dy }); onMove?.(dx / radius, dy / radius); }; const release = () => { setKnob({ x: 0, y: 0 }); setActive(false); onRelease?.(); }; return (
{ (e.target as HTMLElement).setPointerCapture?.(e.pointerId); setActive(true); update(e.clientX, e.clientY); }} onPointerMove={(e) => active && update(e.clientX, e.clientY)} onPointerUp={release} onPointerCancel={release} {...props} >
); }