import React, { useEffect, useRef, useState } from "react"; import { Animated, Easing, StyleSheet, TextStyle, View, ViewStyle, } from "react-native"; import { Circle, G, Path, Svg, Text as SvgText } from "react-native-svg"; export interface SpinWheelItem { id: string | number; label: string; value?: any; color: string; textColor?: string; } const colors = [ "#FF0000", "#FFA500", "#FFFF00", "#008000", "#0000FF", "#800080", "#FFC0CB", "#00FFFF", "#FF00FF", "#00FF00", "#4B0082", "#EE82EE", "#40E0D0", "#FFD700", "#C0C0C0", "#FFDAB9", "#E6E6FA", "#008080", "#FF7F50", "#DC143C", "#87CEEB", "#7FFF00", "#CCCCFF", "#FF6347", "#FA8072", ]; interface SpinWheelProps { items: SpinWheelItem[]; size?: number; winner?: SpinWheelItem | string | number; spinDuration?: number; onSpinStart?: () => void; onSpinEnd?: (item: SpinWheelItem) => void; containerStyle?: ViewStyle; centerStyle?: ViewStyle; wheelTextStyle?: TextStyle; knobStyle?: ViewStyle; wheelBorderColor?: string; wheelBorderWidth?: number; wheelTextColor?: string; knobColor?: string; centerSize?: number; centerComponent?: React.ReactNode; } // Config for the single animation const TOTAL_SPINS = 5; // How many full rotations before stopping const SPIN_DURATION = 4000; // Total time for the one animation const SpinWheel: React.FC = ({ items, size = 300, winner, onSpinEnd, containerStyle, centerStyle, wheelTextStyle, knobStyle, knobColor = "#D81E5B", wheelBorderColor, wheelBorderWidth, wheelTextColor = "#FFFFFF", centerSize, centerComponent, }) => { const wheelItems = items.length > 0 ? items : []; const [spinning, setSpinning] = useState(false); // 0-1 represents 0-360 degrees. We keep increasing this value. const rotateValue = useRef(new Animated.Value(0)).current; const rotationRef = useRef(0); // Sync the ref with the animated value to ensure we start next spin from where we left off useEffect(() => { const listener = rotateValue.addListener(({ value }) => { rotationRef.current = value; }); return () => { rotateValue.removeListener(listener); }; }, [rotateValue]); // Main Logic: Trigger spin when 'winner' is provided useEffect(() => { if (winner != null && !spinning) { spinToWinner(winner); } }, [winner]); const spinToWinner = (winnerValue: SpinWheelItem | string | number) => { const winnerId = typeof winnerValue === "object" ? winnerValue.id : winnerValue; const index = items.findIndex((i) => i.id === winnerId); if (index === -1) { console.warn("Winner not found in items array"); return; } setSpinning(true); // 1. Calculate the angle where the item is centered const anglePerItem = 360 / items.length; // We want the item centered at 270 degrees (top of the wheel in standard SVG) const segmentCenter = (index + 0.5) * anglePerItem; const targetAngle = (270 - segmentCenter + 360) % 360; // 2. Calculate offset from current position const currentRotationDegrees = (rotationRef.current * 360) % 360; const offset = (targetAngle - currentRotationDegrees + 360) % 360; // 3. Calculate total rotation: // Current Rotation + (Distance to target) + (Extra Full Spins) // We divide by 360 because our Animated.Value goes from 0 to 1 for a full circle const finalRotation = rotationRef.current + (offset + 360 * TOTAL_SPINS) / 360; // 4. One smooth animation Animated.timing(rotateValue, { toValue: finalRotation, duration: SPIN_DURATION, easing: Easing.out(Easing.cubic), // Starts fast, slows down smoothly useNativeDriver: true, }).start(() => { rotationRef.current = finalRotation; setSpinning(false); onSpinEnd?.(items[index]); }); }; const anglePerItem = wheelItems.length > 0 ? 360 / wheelItems.length : 0; const generateWheelPaths = () => { if (wheelItems.length === 0) return []; return wheelItems.map((item, index) => { const startAngle = index * anglePerItem; const endAngle = (index + 1) * anglePerItem; const startRad = (startAngle * Math.PI) / 180; const endRad = (endAngle * Math.PI) / 180; const x1 = size / 2 + (size / 2) * Math.cos(startRad); const y1 = size / 2 + (size / 2) * Math.sin(startRad); const x2 = size / 2 + (size / 2) * Math.cos(endRad); const y2 = size / 2 + (size / 2) * Math.sin(endRad); const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1"; const pathData = [ `M ${size / 2} ${size / 2}`, `L ${x1} ${y1}`, `A ${size / 2} ${size / 2} 0 ${largeArcFlag} 1 ${x2} ${y2}`, "Z", ].join(" "); const midRad = ((startAngle + endAngle) / 2) * (Math.PI / 180); const textX = size / 2 + size * 0.32 * Math.cos(midRad); const textY = size / 2 + size * 0.32 * Math.sin(midRad); return { path: pathData, item, textX, textY, angle: (startAngle + endAngle) / 2, }; }); }; const wheelPaths = generateWheelPaths(); const rotate = rotateValue.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "360deg"], }); const actualCenterSize = centerSize !== undefined ? centerSize : size / 5; return ( {wheelPaths.map(({ path, item, textX, textY, angle }) => { return ( {item.label} ); })} {wheelBorderWidth ? ( ) : ( )} {centerComponent ? ( {centerComponent} ) : ( )} ); }; const styles = StyleSheet.create({ container: { alignItems: "center", justifyContent: "center", marginTop: 20, marginBottom: 70, }, wheelContainer: { overflow: "hidden", backgroundColor: "transparent", }, wheelCenter: { position: "absolute", top: "50%", left: "50%", backgroundColor: "#000000", borderWidth: 1, borderColor: "#333333", zIndex: 1, }, pointerPosition: { position: "absolute", left: "50%", transform: [{ translateX: -10 }, { rotate: "180deg" }], zIndex: 2, }, pointer: { width: 0, height: 0, backgroundColor: "transparent", borderStyle: "solid", borderLeftWidth: 10, borderRightWidth: 10, borderBottomWidth: 15, borderLeftColor: "transparent", borderRightColor: "transparent", }, actionButton: { paddingHorizontal: 30, paddingVertical: 12, borderRadius: 25, shadowColor: "#000", shadowRadius: 3, backgroundColor: "grey", }, actionButtonText: { fontWeight: "bold", fontSize: 16, }, }); export default SpinWheel;