"use client"; import React, { useState, useRef, useEffect } from "react"; interface QuestionWheelProps { questions: string[]; } const COLORS = [ "#3369e8", // Blue "#d50f25", // Red "#eeb211", // Yellow "#009925", // Green "#ff6600", // Orange "#9933cc", // Purple "#00ccff", // Cyan "#ff3399", // Pink ]; export default function SpinningPickerWheel({ questions }: QuestionWheelProps) { // Removed rotation state to avoid re-renders const [isSpinning, setIsSpinning] = useState(false); const [selectedQuestion, setSelectedQuestion] = useState(null); const [isWaitingForModal, setIsWaitingForModal] = useState(false); const [isClosing, setIsClosing] = useState(false); const [isMounted, setIsMounted] = useState(false); const [isSoundEnabled, setIsSoundEnabled] = useState(true); const wheelRef = useRef(null); const idleRotationRef = useRef(0); const audioRef = useRef(null); const animationFrameRef = useRef(undefined); // Limit questions to a reasonable number for the wheel (e.g., 20) to avoid overcrowding // If we have more, we can shuffle and pick the top 20, or just use them all if the user insists. // For readability, let's cap at 24. const displayQuestions = questions.slice(0, 24); const numSegments = displayQuestions.length; const anglePerSegment = 360 / numSegments; // Set mounted state and initialize audio useEffect(() => { audioRef.current = new Audio('/spin-sound.mp3'); audioRef.current.loop = false; // Don't loop the sound // Use a timeout to avoid setState in effect const timer = setTimeout(() => setIsMounted(true), 0); return () => clearTimeout(timer); }, []); // Idle rotation effect useEffect(() => { if (!isMounted || isSpinning || selectedQuestion || isWaitingForModal) return; const rotateIdle = () => { idleRotationRef.current += 0.1; // Slow rotation speed // Direct DOM manipulation for performance if (wheelRef.current) { wheelRef.current.style.transform = `rotate(${idleRotationRef.current}deg)`; } animationFrameRef.current = requestAnimationFrame(rotateIdle); }; animationFrameRef.current = requestAnimationFrame(rotateIdle); return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } }; }, [isMounted, isSpinning, selectedQuestion, isWaitingForModal]); const spinWheel = () => { if (isSpinning || selectedQuestion || isWaitingForModal) return; setIsSpinning(true); setSelectedQuestion(null); // Cancel idle animation if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } // Play sound if enabled if (isSoundEnabled && audioRef.current) { audioRef.current.currentTime = 0; audioRef.current.play().catch(e => console.log("Audio play failed:", e)); } // Randomly select a winner index const winnerIndex = Math.floor(Math.random() * numSegments); // Calculate rotation // We want the winner segment to end up at the pointer (Left side = 180 degrees) // The wheel rotates clockwise. // Segment i center is at (i + 0.5) * anglePerSegment. // We want (i + 0.5) * anglePerSegment + Rotation = 180 (mod 360). // Rotation = 180 - segmentCenter. const extraSpins = 5; const baseSpin = 360 * extraSpins; const segmentCenter = (winnerIndex * anglePerSegment) + (anglePerSegment / 2); const currentRotation = idleRotationRef.current; // We want: (segmentCenter + finalRotation) % 360 === 180 // finalRotation % 360 === (180 - segmentCenter) % 360 const targetAngle = (180 - segmentCenter); const normalizedTargetAngle = ((targetAngle % 360) + 360) % 360; const currentAngle = currentRotation % 360; const normalizedCurrentAngle = ((currentAngle % 360) + 360) % 360; let rotationNeeded = normalizedTargetAngle - normalizedCurrentAngle; if (rotationNeeded < 0) { rotationNeeded += 360; } // Calculate final rotation from current position const newRotation = currentRotation + rotationNeeded + baseSpin; // Update ref for next time idleRotationRef.current = newRotation; // Apply rotation with transition if (wheelRef.current) { // Force reflow to ensure transition happens if we just set it // void wheelRef.current.offsetWidth; wheelRef.current.style.transition = "transform 2.5s cubic-bezier(0.25, 0.1, 0.25, 1)"; wheelRef.current.style.transform = `rotate(${newRotation}deg)`; } setTimeout(() => { setIsSpinning(false); // Remove transition for idle rotation if (wheelRef.current) { wheelRef.current.style.transition = "none"; } // Stop sound if (audioRef.current) { audioRef.current.pause(); audioRef.current.currentTime = 0; } setIsWaitingForModal(true); // Wait 2 seconds before showing the modal setTimeout(() => { setSelectedQuestion(displayQuestions[winnerIndex]); setIsWaitingForModal(false); }, 1000); }, 2700); // 2.5 seconds spin duration + buffer }; const closeModal = () => { setIsClosing(true); setTimeout(() => { setSelectedQuestion(null); setIsClosing(false); }, 300); // Match animation duration }; const getSegmentPath = (index: number) => { const startAngle = index * anglePerSegment; const endAngle = (index + 1) * anglePerSegment; // Convert to radians const startRad = (Math.PI / 180) * startAngle; const endRad = (Math.PI / 180) * endAngle; const radius = 200; // Wheel radius const center = 200; // SVG center const x1 = center + radius * Math.cos(startRad); const y1 = center + radius * Math.sin(startRad); const x2 = center + radius * Math.cos(endRad); const y2 = center + radius * Math.sin(endRad); // Path command return `M ${center} ${center} L ${x1} ${y1} A ${radius} ${radius} 0 0 1 ${x2} ${y2} Z`; }; return (
{/* Pointer - Left Side */} {/* Wheel */}
{/* rotate-90 to start index 0 at 12 o'clock if we wanted, but we are using 0 degrees = 3 o'clock. Actually, standard SVG 0 is 3 o'clock. If we want index 0 to start at 0 degrees, we don't need rotation. */} {isMounted && displayQuestions.map((q, i) => ( {q.slice(0, 22) + (q.length > 22 ? "..." : "")} ))}
{/* Center Spin Button */} {/* Sound Toggle Button */}
{/* Result Modal */} {selectedQuestion && (
e.stopPropagation()} >

The Question Is:

{selectedQuestion}

)}
); }