UNPKG

@seirei/carousel

Version:

A reusable, auto-scrolling carousel component for React Native built using **React Native Reanimated v2**, offering customizable pagination indicators, smooth animations, and infinite looping.

127 lines (122 loc) 4.42 kB
"use strict"; import React, { useMemo } from "react"; import { View, StyleSheet } from "react-native"; import Animated, { cancelAnimation, runOnJS, useAnimatedReaction, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from "react-native-reanimated"; import { SCREEN_WIDTH } from "./constants.js"; /** * CarouselWrapper is a React component that implements a horizontally scrollable carousel * with pagination indicators. It supports auto-scrolling and customizable styles. * * @param {CarouselWrapperProps} props - The properties for the carousel. * @returns {JSX.Element} The rendered carousel component. */ export const CarouselWrapper = ({ children, wrapperStyle, autoSlide = false, snapDuration = 1000, activeSlideAccentColor = "#00000070", inactiveSlideAccentColor = "#D3D3D350", dotSize = 10 }) => { const totalSlides = children.length; const scrollX = useSharedValue(0); const autoScrollProgress = useSharedValue(0); const [activeIndex, setActiveIndex] = React.useState(0); const scrollViewRef = React.useRef(null); /* ---------- scrolling ---------- */ const scrollHandler = useAnimatedScrollHandler(event => { scrollX.value = event.contentOffset.x; runOnJS(setActiveIndex)(Math.round(event.contentOffset.x / SCREEN_WIDTH)); }); const scrollToIndex = index => { scrollViewRef.current?.scrollTo({ x: index * SCREEN_WIDTH, animated: true }); }; /* ---------- autoplay ---------- */ useMemo(() => { if (autoSlide) { autoScrollProgress.value = withRepeat(withTiming(1, { duration: snapDuration }), -1, true); } return () => cancelAnimation(autoScrollProgress); }, [autoScrollProgress, snapDuration, autoSlide]); useAnimatedReaction(() => autoScrollProgress.value, progress => { if (!autoSlide || progress !== 1) return; if (progress === 1) { const nextIndex = (activeIndex + 1) % totalSlides; runOnJS(scrollToIndex)(nextIndex); } }, [activeIndex, totalSlides]); /* ---------- pagination helpers ---------- */ const getVisibleIndices = () => { if (totalSlides <= 3) return [...Array(totalSlides).keys()]; if (activeIndex === 0) return [0, 1, 2]; if (activeIndex === totalSlides - 1) return [totalSlides - 3, totalSlides - 2, totalSlides - 1]; return [activeIndex - 1, activeIndex, activeIndex + 1]; }; const visibleIndices = getVisibleIndices(); /* ---------- render ---------- */ return /*#__PURE__*/React.createElement(Animated.View, { style: [{ overflow: "hidden" }, wrapperStyle] }, /*#__PURE__*/React.createElement(Animated.ScrollView, { decelerationRate: "fast", horizontal: true, onScroll: scrollHandler, ref: scrollViewRef, scrollEventThrottle: 16, showsHorizontalScrollIndicator: false, snapToInterval: SCREEN_WIDTH }, children.map((child, index) => /*#__PURE__*/React.createElement(View, { key: index, style: { width: SCREEN_WIDTH } }, child))), /*#__PURE__*/React.createElement(Animated.View, { style: styles.paginationContainer }, visibleIndices.map((slideIndex, i) => { // Which dot should be “focussed”? const isActive = totalSlides <= 3 && activeIndex === slideIndex || totalSlides > 3 && (activeIndex === 0 && i === 0 || activeIndex === totalSlides - 1 && i === 2 || activeIndex !== 0 && activeIndex !== totalSlides - 1 && i === 1); const animatedScale = useSharedValue(isActive ? 1.2 : 1); useAnimatedReaction(() => activeIndex, () => { animatedScale.value = withTiming(isActive ? 1.2 : 1, { duration: 300 }); }, [isActive, activeIndex]); const animatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: animatedScale.value }] })); return /*#__PURE__*/React.createElement(Animated.View, { key: i, style: [styles.dot, { width: dotSize, height: dotSize, borderRadius: dotSize / 2, backgroundColor: isActive ? activeSlideAccentColor : inactiveSlideAccentColor }, animatedStyle] }); }))); }; const styles = StyleSheet.create({ paginationContainer: { position: "absolute", bottom: 15, left: 0, right: 0, flexDirection: "row", justifyContent: "center", alignItems: "center", gap: 8 }, dot: { opacity: 1 } }); //# sourceMappingURL=CarouselWrapper.js.map