import { useEffect, useState } from "react"; import { StyleSheet, Text, View } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { runOnJS, useAnimatedStyle, useSharedValue, } from "react-native-reanimated"; import { Palette, Style } from "../styles"; import { FONT_FAMILY } from "../styles/Fonts"; import { LinearGradient } from "./LinearGradient/LinearGradient"; const INITIAL_BOX_SIZE = 6; export default ({ value, maxValue, progress, onSeek, onSeekStart, onSeekEnd, seekEnabled = false, }) => { const offset = useSharedValue(0); const boxWidth = useSharedValue(INITIAL_BOX_SIZE); const [layout, setLayout] = useState(null); const SLIDER_WIDTH = layout?.width; const MAX_VALUE = SLIDER_WIDTH - INITIAL_BOX_SIZE; const pan = Gesture.Pan() .enabled(seekEnabled) .onBegin(() => { if (seekEnabled && typeof onSeekStart === "function") { // Notify JS thread that user started seeking (e.g., pause audio) runOnJS(onSeekStart)(); } }) .onChange((event) => { offset.value = Math.abs(offset.value) <= MAX_VALUE ? offset.value + event.changeX <= 0 ? 0 : offset.value + event.changeX >= MAX_VALUE ? MAX_VALUE : offset.value + event.changeX : offset.value; const newWidth = INITIAL_BOX_SIZE + offset.value; boxWidth.value = newWidth; }) .onEnd(() => { if (!seekEnabled || !onSeek || !MAX_VALUE) return; const ratio = MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0; // Reanimated -> JS thread bridge runOnJS(onSeek)(ratio); }) .onFinalize(() => { if (seekEnabled && typeof onSeekEnd === "function") { runOnJS(onSeekEnd)(); } }); // Reflect external progress into the slider UI useEffect(() => { if (typeof progress === "number" && layout?.width) { const max = layout.width - INITIAL_BOX_SIZE; const clamped = Math.max(0, Math.min(1, progress)); const newOffset = clamped * max; offset.value = newOffset; boxWidth.value = INITIAL_BOX_SIZE + newOffset; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [progress, layout?.width]); const boxStyle = useAnimatedStyle(() => { return { width: INITIAL_BOX_SIZE + offset.value, }; }); const sliderStyle = useAnimatedStyle(() => { return { transform: [{ translateX: offset.value }], }; }); return ( setLayout(e.nativeEvent.layout)}> {value} {maxValue} ); }; const styles = StyleSheet.create({ box: { height: INITIAL_BOX_SIZE, borderRadius: 20, position: "absolute", zIndex: 1, }, sliderHandle: { width: 20, height: 20, backgroundColor: "#f8f9ff", borderRadius: 25, position: "absolute", zIndex: 2, borderWidth: 4, borderColor: "#9B4DFF", shadowColor: "#8951FC", shadowOffset: { width: 0, height: 3, }, shadowOpacity: 0.17, shadowRadius: 3.05, elevation: 4, }, sliderTrack: { height: 6, backgroundColor: "#0F0C19", borderRadius: 25, justifyContent: "center", }, time: { fontSize: 14, color: Palette.white, fontFamily: FONT_FAMILY.InterRegular, }, });