'use client' import { useEffect, useRef, useState } from 'react' interface CompassSelectorProps { value: number // 0-360 degrees onChange: (value: number) => void range?: number // Optional range to display as arc (±degrees) size?: number className?: string } export function CompassSelector({ value, onChange, range, size = 200, className = '' }: CompassSelectorProps) { const [isDragging, setIsDragging] = useState(false) const compassRef = useRef(null) const handlePointerMove = (e: PointerEvent) => { if (!isDragging || !compassRef.current) return const rect = compassRef.current.getBoundingClientRect() const centerX = rect.left + rect.width / 2 const centerY = rect.top + rect.height / 2 const dx = e.clientX - centerX const dy = e.clientY - centerY // Calculate angle in degrees (0° = North/top) let angle = Math.atan2(dx, -dy) * (180 / Math.PI) if (angle < 0) angle += 360 // Round to nearest 5 degrees for easier selection angle = Math.round(angle / 5) * 5 onChange(angle % 360) } const handlePointerDown = (e: React.PointerEvent) => { setIsDragging(true) // Trigger initial update const rect = compassRef.current!.getBoundingClientRect() const centerX = rect.left + rect.width / 2 const centerY = rect.top + rect.height / 2 const dx = e.clientX - centerX const dy = e.clientY - centerY let angle = Math.atan2(dx, -dy) * (180 / Math.PI) if (angle < 0) angle += 360 angle = Math.round(angle / 5) * 5 onChange(angle % 360) } const handlePointerUp = () => { setIsDragging(false) } useEffect(() => { if (isDragging) { window.addEventListener('pointermove', handlePointerMove) window.addEventListener('pointerup', handlePointerUp) return () => { window.removeEventListener('pointermove', handlePointerMove) window.removeEventListener('pointerup', handlePointerUp) } } }, [isDragging]) // Convert degrees to radians for calculations const angleRad = (value - 90) * (Math.PI / 180) const radius = size / 2 - 10 const needleLength = radius * 0.7 // Calculate needle endpoint const needleX = size / 2 + Math.cos(angleRad) * needleLength const needleY = size / 2 + Math.sin(angleRad) * needleLength // Helper to convert degrees to compass direction const getCompassDirection = (degrees: number): string => { const directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'] const index = Math.round(degrees / 22.5) % 16 return directions[index] } return (
{/* Outer circle */} {/* Acceptable range arc (green) */} {range !== undefined && (() => { const startAngle = value - range const endAngle = value + range // Convert to radians for calculation (adjusting for SVG coordinate system) const startRad = (startAngle - 90) * (Math.PI / 180) const endRad = (endAngle - 90) * (Math.PI / 180) // Calculate start and end points on the circle const x1 = size / 2 + Math.cos(startRad) * radius const y1 = size / 2 + Math.sin(startRad) * radius const x2 = size / 2 + Math.cos(endRad) * radius const y2 = size / 2 + Math.sin(endRad) * radius // Determine if the arc should be large (>180°) or small const largeArcFlag = range * 2 > 180 ? 1 : 0 // Create SVG arc path const arcPath = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2}` return ( ) })()} {/* Cardinal direction markers */} {[ { angle: 0, label: 'N' }, { angle: 90, label: 'E' }, { angle: 180, label: 'S' }, { angle: 270, label: 'W' }, ].map(({ angle, label }) => { const rad = (angle - 90) * (Math.PI / 180) const x = size / 2 + Math.cos(rad) * (radius - 20) const y = size / 2 + Math.sin(rad) * (radius - 20) return ( {label} ) })} {/* Degree tick marks every 30° */} {Array.from({ length: 12 }, (_, i) => { const angle = i * 30 const rad = (angle - 90) * (Math.PI / 180) const x1 = size / 2 + Math.cos(rad) * (radius - 5) const y1 = size / 2 + Math.sin(rad) * (radius - 5) const x2 = size / 2 + Math.cos(rad) * radius const y2 = size / 2 + Math.sin(rad) * radius return ( ) })} {/* Center dot */} {/* Direction needle */} {/* Needle tip */} {/* Value display */}
{value}° ({getCompassDirection(value)})
) }