@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
JavaScript
"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