import React, { memo, useMemo, useRef } from 'react';
import {
Animated,
PanResponder,
StyleSheet,
View,
Platform,
Text,
} from 'react-native';
import { sin } from './animated-math';
import { CONTAINER_HEIGHT } from '../../enums';
import { ClassNames, Styles, PickerOption } from '../../types';
import { isEqual } from 'lodash';
interface WheelProps {
value: number | string;
setValue?: (value: any) => void;
items: PickerOption[];
styles?: Styles;
classNames?: ClassNames;
}
const ITEM_HEIGHT = 44;
const WheelWeb = ({
value,
setValue = () => {},
items,
styles = {},
classNames = {},
}: WheelProps) => {
const displayCount = 5;
const translateY = useRef(new Animated.Value(0)).current;
const height = 140;
const radius = height / 2;
// Add padding to items like native implementation
const paddedItems = useMemo(() => {
const padding = Math.max(0, Math.floor(displayCount / 2));
const array: (PickerOption | null)[] = [...items];
for (let i = 0; i < padding; i++) {
array.unshift(null);
array.push(null);
}
return array;
}, [items, displayCount]);
const renderCount =
displayCount * 2 < paddedItems.length ? displayCount * 8 : displayCount * 2 - 1;
const circular = items.length >= displayCount;
const valueIndex = useMemo(() => {
const padding = Math.max(0, Math.floor(displayCount / 2));
const rawIndex = items.findIndex((item) => item.value === value);
return rawIndex >= 0 ? rawIndex + padding : padding;
}, [items, value, displayCount]);
const panResponder = useMemo(() => {
return PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onStartShouldSetPanResponderCapture: () => true,
onPanResponderGrant: () => {
translateY.setValue(0);
},
onPanResponderMove: (evt, gestureState) => {
translateY.setValue(gestureState.dy);
evt.stopPropagation();
},
onPanResponderRelease: (_, gestureState) => {
translateY.extractOffset();
const padding = Math.max(0, Math.floor(displayCount / 2));
let newValueIndex =
valueIndex -
Math.round(gestureState.dy / ((radius * 2) / displayCount));
// Adjust for padding
if (circular) {
newValueIndex = (newValueIndex + paddedItems.length) % paddedItems.length;
} else {
newValueIndex = Math.max(padding, Math.min(newValueIndex, paddedItems.length - padding - 1));
}
const newValue = paddedItems[newValueIndex];
if (newValue?.value === value) {
translateY.setOffset(0);
translateY.setValue(0);
} else if (newValue?.value !== undefined && newValue?.value !== null) {
setValue(newValue.value);
} else {
// If we landed on a null (padding), find the nearest real value
const realIndex = newValueIndex < padding ? padding : paddedItems.length - padding - 1;
const realValue = paddedItems[realIndex];
if (realValue?.value !== undefined && realValue?.value !== null) {
setValue(realValue.value);
}
}
},
});
}, [
circular,
displayCount,
radius,
setValue,
value,
valueIndex,
paddedItems,
translateY,
]);
const displayValues = useMemo(() => {
const centerIndex = Math.floor(renderCount / 2);
return Array.from({ length: renderCount }, (_, index) => {
let targetIndex = valueIndex + index - centerIndex;
if (circular) {
targetIndex =
((targetIndex % paddedItems.length) + paddedItems.length) % paddedItems.length;
} else {
targetIndex = Math.max(0, Math.min(targetIndex, paddedItems.length - 1));
}
return paddedItems[targetIndex] || null;
});
}, [renderCount, valueIndex, paddedItems, circular]);
const animatedAngles = useMemo(() => {
//translateY.setValue(0);
translateY.setOffset(0);
const currentIndex = displayValues.findIndex(
(item) => item?.value === value
);
return displayValues && displayValues.length > 0
? displayValues.map((_, index) =>
translateY
.interpolate({
inputRange: [-radius, radius],
outputRange: [
-radius +
((radius * 2) / displayCount) * (index - currentIndex),
radius + ((radius * 2) / displayCount) * (index - currentIndex),
],
extrapolate: 'extend',
})
.interpolate({
inputRange: [-radius, radius],
outputRange: [-Math.PI / 2, Math.PI / 2],
extrapolate: 'clamp',
})
)
: [];
}, [displayValues, radius, value, displayCount, translateY]);
return (
{displayValues?.map((displayValue, index) => {
const animatedAngle = animatedAngles[index];
return (
{displayValue?.text}
);
})}
);
};
const defaultStyles = StyleSheet.create({
container: {
minWidth: 80,
paddingHorizontal: 8,
overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
height: CONTAINER_HEIGHT / 2,
...Platform.select({
web: {
cursor: 'pointer',
userSelect: 'none',
},
}),
},
contentContainer: {
justifyContent: 'space-between',
alignItems: 'center',
},
selectedIndicator: {
position: 'absolute',
width: '100%',
top: '50%',
},
});
const customComparator = (
prev: Readonly,
next: Readonly
) => {
const areEqual =
prev.value === next.value &&
prev.setValue === next.setValue &&
isEqual(prev.styles, next.styles) &&
isEqual(prev.classNames, next.classNames) &&
isEqual(prev.items, next.items);
return areEqual;
};
export default memo(WheelWeb, customComparator);