diff --git a/README.md b/README.md
index 410d105..5c027ac 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,8 @@
A highly customizable, performant carousel component for React Native with advanced animations, auto-scrolling capabilities, and infinite scrolling support. Built with React Native Reanimated for smooth, native-level performance.
-**✨ Context-Based Configuration** - All carousel settings are configured through the context provider for a clean, centralized API.
+**✨ Compound Pattern** - Clean, intuitive API with `HeroCarousel.Provider`, `HeroCarousel.Item`, and `HeroCarousel.AnimatedView`
+**✨ Context-Based Configuration** - All carousel settings are configured through the provider for a clean, centralized API.
## Features
@@ -27,7 +28,7 @@ npm install @strv/react-native-hero-carousel
# or
yarn add @strv/react-native-hero-carousel
# or
-yarn add @strv/react-native-hero-carousel
+pnpm add @strv/react-native-hero-carousel
```
### Peer Dependencies
@@ -45,7 +46,7 @@ Make sure to follow the [React Native Reanimated installation guide](https://doc
```tsx
import React from 'react'
import { View, Text, StyleSheet } from 'react-native'
-import { HeroCarousel, CarouselContextProvider } from '@strv/react-native-hero-carousel'
+import { HeroCarousel } from '@strv/react-native-hero-carousel'
const slides = [
{ id: 1, title: 'Slide 1', color: '#FF6B6B' },
@@ -62,7 +63,7 @@ const Slide = ({ title, color }: { title: string; color: string }) => (
export default function BasicCarousel() {
return (
-
+
{slides.map((slide) => (
@@ -70,7 +71,7 @@ export default function BasicCarousel() {
))}
-
+
)
}
@@ -95,12 +96,22 @@ const styles = StyleSheet.create({
### Components
-#### `CarouselContextProvider`
+The `HeroCarousel` component uses a **compound pattern** that provides a clean, intuitive API:
+
+```tsx
+
+
+ {/* Your slide content */}
+
+
+```
+
+#### `HeroCarousel.Provider`
The context provider that must wrap your carousel components. **All carousel configuration is passed here.**
```tsx
- withTiming(to, { duration })} // Custom animation
>
{children}
-
+
```
**Props:**
@@ -126,7 +137,7 @@ The context provider that must wrap your carousel components. **All carousel con
#### `HeroCarousel`
-The main carousel component that renders slides. **Takes no configuration props** - all configuration is handled by the context.
+The main carousel component that renders slides. **Takes no configuration props** - all configuration is handled by the context provider.
```tsx
@@ -142,6 +153,42 @@ The main carousel component that renders slides. **Takes no configuration props*
| ---------- | ------------------- | ------------------------- |
| `children` | `React.ReactNode[]` | Array of slide components |
+#### `HeroCarousel.Item`
+
+A wrapper component for individual slides. Provides slide context to child components. **Note:** This is automatically used internally when you pass children to `HeroCarousel`, but you can use it directly for more control.
+
+```tsx
+{/* Your slide content */}
+```
+
+#### `HeroCarousel.AnimatedView`
+
+A specialized animated view component that automatically handles entering/exiting animations based on carousel scroll position. Perfect for creating slide-specific animations.
+
+```tsx
+import { FadeIn, FadeOut } from 'react-native-reanimated'
+;
+ This animates when the slide becomes active
+
+```
+
+**Props:**
+
+| Prop | Type | Default | Description |
+| ------------------------- | -------------------------------------- | -------- | -------------------------------------------------------- |
+| `children` | `React.ReactNode` | Required | Content to animate |
+| `entering` | `AnimatedProps['entering']` | - | Entering animation (from react-native-reanimated) |
+| `exiting` | `AnimatedProps['exiting']` | - | Exiting animation (from react-native-reanimated) |
+| `layout` | `AnimatedProps['layout']` | - | Layout animation (from react-native-reanimated) |
+| `enteringThreshold` | `number` | `0.99` | Threshold (0-1) when entering animation should trigger |
+| `exitingThreshold` | `number` | `0.01` | Threshold (0-1) when exiting animation should trigger |
+| `keepVisibleAfterExiting` | `boolean` | `false` | Keep component visible after exiting animation completes |
+| `style` | `AnimatedProps['style']` | - | Additional styles |
+
### Hooks
#### `useCarouselContext()`
@@ -161,12 +208,12 @@ const { scrollValue, timeoutValue, slideWidth, userInteracted, setUserInteracted
- `userInteracted`: Boolean indicating if user has interacted with carousel
- `setUserInteracted`: Function to update interaction state
-#### `useHeroCarouselSlideIndex()`
+#### `useAutoCarouselSlideIndex()`
-Get the current slide information and auto-scroll controls.
+Get the current slide information and auto-scroll controls. Must be used within a slide component (inside `HeroCarousel`).
```tsx
-const { index, total, runAutoScroll, goToPage } = useHeroCarouselSlideIndex()
+const { index, total, runAutoScroll, goToPage } = useAutoCarouselSlideIndex()
```
**Returns:**
@@ -176,30 +223,52 @@ const { index, total, runAutoScroll, goToPage } = useHeroCarouselSlideIndex()
- `runAutoScroll`: Function to manually trigger auto-scroll with custom interval
- `goToPage`: Function to programmatically navigate to a specific slide from another slide
-### Utilities
-
-#### `interpolateInsideCarousel()`
+#### `useInterpolateInsideCarousel()`
-Advanced interpolation utility for creating custom slide animations.
+Hook for creating custom slide animations with automatic interpolation based on carousel scroll position. Must be used within a slide component (inside `HeroCarousel`). Returns a `SharedValue` that you can use in animated styles.
```tsx
-import { interpolateInsideCarousel } from '@strv/react-native-hero-carousel'
+import { useInterpolateInsideCarousel } from '@strv/react-native-hero-carousel'
+import Animated, { useAnimatedStyle, interpolate, Extrapolation } from 'react-native-reanimated'
-const animatedStyle = useAnimatedStyle(() => {
- const progress = interpolateInsideCarousel(scrollValue.value, slideIndex, total, {
+const Slide = () => {
+ const progress = useInterpolateInsideCarousel({
valueBefore: 0, // Value for slides before current
thisValue: 1, // Value for current slide
valueAfter: 0, // Value for slides after current
offset: 0.2, // Animation offset (optional)
})
- return {
- opacity: progress,
- transform: [{ scale: progress }],
- }
-})
+ const animatedStyle = useAnimatedStyle(() => {
+ return {
+ opacity: progress.value,
+ transform: [
+ {
+ scale: interpolate(progress.value, [0, 1], [0.8, 1], Extrapolation.CLAMP),
+ },
+ ],
+ }
+ })
+
+ return {/* Your content */}
+}
```
+**Returns:**
+
+A `SharedValue` (from `useDerivedValue`) representing the interpolated progress value (0-1) based on the current slide's position in the carousel. Access the value using `.value` in animated styles or worklet functions.
+
+**Parameters:**
+
+| Parameter | Type | Default | Description |
+| ------------- | -------- | -------- | ---------------------------------------------------------------- |
+| `valueBefore` | `number` | Required | Value to use for slides before the current slide |
+| `thisValue` | `number` | Required | Value to use for the current slide |
+| `valueAfter` | `number` | Required | Value to use for slides after the current slide |
+| `offset` | `number` | `0` | Animation offset (0-1) to control when the animation starts/ends |
+
+**Note:** The `interpolateInsideCarousel` utility function is now internal-only. Use this hook instead for all custom animations. The hook automatically handles the slide context (index, total, scrollValue) internally.
+
## Examples
We provide a comprehensive example app showcasing all the carousel features. You can run the examples locally or view the source code:
@@ -216,23 +285,25 @@ Then scan the QR code with Expo Go or run on simulator. See the [example app REA
### 📱 Available Examples
-| Example | Description | Source Code |
-| ---------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------- |
-| **Basic Carousel** | Simple auto-scrolling image carousel | [`BasicExample.tsx`](./example/examples/BasicExample.tsx) |
-| **Animated Carousel** | Custom animations with scale, rotation, and opacity | [`AnimatedExample.tsx`](./example/examples/AnimatedExample.tsx) |
-| **Video Carousel** | Video playback with play/pause controls | [`VideoCarouselExample.tsx`](./example/examples/VideoCarouselExample.tsx) |
-| **Timer Pagination** | Visual progress indicators with custom intervals | [`TimerPaginationExample.tsx`](./example/examples/TimerPaginationExample.tsx) |
-| **Entering Animation** | Advanced slide entrance animations | [`EnteringAnimationExample.tsx`](./example/examples/EnteringAnimationExample.tsx) |
-| **Offset Example** | Custom slide positioning and spacing | [`OffsetExample.tsx`](./example/examples/OffsetExample.tsx) |
+| Example | Description | Source Code |
+| ---------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
+| **Basic Carousel** | Simple auto-scrolling image carousel | [`BasicExample.tsx`](./example/examples/BasicExample.tsx) |
+| **Animated Carousel** | Custom animations with scale, rotation, and opacity | [`AnimatedExample.tsx`](./example/examples/AnimatedExample.tsx) |
+| **Video Carousel** | Video playback with play/pause controls | [`VideoCarouselExample.tsx`](./example/examples/VideoCarouselExample.tsx) |
+| **Timer Pagination** | Visual progress indicators with custom intervals | [`TimerPaginationExample.tsx`](./example/examples/TimerPaginationExample.tsx) |
+| **Entering Animation** | Advanced slide entrance animations using `HeroCarousel.AnimatedView` | [`EnteringAnimationExample.tsx`](./example/examples/EnteringAnimationExample.tsx) |
+| **Offset Example** | Custom slide positioning and spacing | [`OffsetExample.tsx`](./example/examples/OffsetExample.tsx) |
### 🎯 Key Example Features
- **Image Carousels** with smooth transitions and auto-scrolling
- **Video Integration** with `expo-video` and playback controls
-- **Custom Animations** using `interpolateInsideCarousel` utility
+- **Custom Animations** using `useInterpolateInsideCarousel` hook
+- **Entering/Exiting Animations** using `HeroCarousel.AnimatedView` component
- **Timer-based Pagination** with visual progress bars
- **Gesture Handling** with swipe navigation and user interaction detection
- **Performance Optimization** with image preloading and memoization
+- **Compound Pattern** - All examples use `HeroCarousel.Provider` for configuration
### 📍 Pagination Examples
@@ -253,38 +324,87 @@ All pagination components automatically sync with the carousel state and support
### Configuration Examples
-Different carousel configurations using the context provider:
+Different carousel configurations using the compound pattern:
```tsx
// Basic auto-scrolling carousel
-
+
{slides}
-
+
// Video carousel without auto-scroll
-
+
{videoSlides}
-
+
// Carousel with custom intervals per slide
- (index + 1) * 2000}>
+ (index + 1) * 2000}>
{slides}
-
+
// Carousel starting from specific slide
-
+
{slides}
-
+
// Custom slide width and animation
- withSpring(to, { damping: 15 })}
>
{slides}
-
+
```
+### Using HeroCarousel.AnimatedView
+
+The `HeroCarousel.AnimatedView` component automatically handles entering/exiting animations based on carousel scroll position. Perfect for creating slide-specific animations:
+
+```tsx
+import { HeroCarousel } from '@strv/react-native-hero-carousel'
+import { FadeIn, FadeOut, SlideInDown } from 'react-native-reanimated'
+
+const Slide = ({ title, image }: { title: string; image: string }) => (
+
+
+
+ {/* Content that animates when slide becomes active */}
+
+ {title}
+
+
+ {/* Multiple animated views with different timings */}
+
+ Subtitle with delay
+
+
+)
+
+// Usage
+
+
+ {slides.map((slide) => (
+
+ ))}
+
+
+```
+
+**Key Features:**
+
+- Automatically triggers entering animation when slide becomes active
+- Triggers exiting animation when slide leaves view
+- Supports all Reanimated entering/exiting animations
+- Configurable thresholds for animation timing
+- Can keep content visible after exiting animation
+
### Programmatic Navigation
Control the carousel programmatically using the context:
@@ -292,7 +412,7 @@ Control the carousel programmatically using the context:
```tsx
const CarouselWithControls = () => {
const { scrollValue, goToPage } = useCarouselContext()
- const { runAutoScroll } = useHeroCarouselSlideIndex()
+ const { runAutoScroll } = useAutoCarouselSlideIndex()
const goToNext = () => {
runAutoScroll(0) // Immediate transition
@@ -303,7 +423,7 @@ const CarouselWithControls = () => {
}
return (
-
+
{/* Your slides */}
@@ -312,7 +432,7 @@ const CarouselWithControls = () => {
-
+
)
}
```
@@ -338,15 +458,34 @@ useEffect(() => {
## Architecture
+### Compound Pattern
+
+This library uses a **compound component pattern** that provides a clean, intuitive API:
+
+```tsx
+
+
+
+ {/* Your content */}
+
+
+
+```
+
### Context-Based Configuration
-This library uses a **context-based architecture** where all carousel configuration is passed to the `CarouselContextProvider` rather than individual components. This design provides several benefits:
+All carousel configuration is passed to the `HeroCarousel.Provider` rather than individual components. This design provides several benefits:
✅ **Centralized Configuration** - All settings in one place
✅ **Cleaner Component API** - Components focus on rendering, not configuration
-✅ **Easier Testing** - Mock context for isolated component testing
+✅ **Easier Testing** - Mock context for isolated component testing
+✅ **Flexible Composition** - Components like pagination can be placed anywhere within the provider
+
+The compound pattern allows for:
-That allows for components like pagination to not be attached to the carousel component.
+- **Intuitive API** - Related components are grouped under `HeroCarousel.*`
+- **Better Discoverability** - All carousel-related components are accessible via autocomplete
+- **Flexible Usage** - Use `HeroCarousel.Item` and `HeroCarousel.AnimatedView` when needed, or pass children directly to `HeroCarousel`
## Troubleshooting
@@ -354,7 +493,7 @@ That allows for components like pagination to not be attached to the carousel co
**Carousel not auto-scrolling:**
-- Ensure `CarouselContextProvider` wraps your carousel
+- Ensure `HeroCarousel.Provider` wraps your carousel
- Check if `disableAutoScroll` is set to `false`
- Verify React Native Reanimated is properly installed
diff --git a/example/components/Collapsible.tsx b/example/components/Collapsible.tsx
deleted file mode 100644
index 0a8c5a1..0000000
--- a/example/components/Collapsible.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { PropsWithChildren, useState } from 'react'
-import { StyleSheet, TouchableOpacity } from 'react-native'
-
-import { ThemedText } from '@/components/ThemedText'
-import { ThemedView } from '@/components/ThemedView'
-import { IconSymbol } from '@/components/ui/IconSymbol'
-import { Colors } from '@/constants/Colors'
-import { useColorScheme } from '@/hooks/useColorScheme'
-
-export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
- const [isOpen, setIsOpen] = useState(false)
- const theme = useColorScheme() ?? 'light'
-
- return (
-
- setIsOpen((value) => !value)}
- activeOpacity={0.8}
- >
-
-
- {title}
-
- {isOpen && {children}}
-
- )
-}
-
-const styles = StyleSheet.create({
- heading: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 6,
- },
- content: {
- marginTop: 6,
- marginLeft: 24,
- },
-})
diff --git a/example/components/ExternalLink.tsx b/example/components/ExternalLink.tsx
deleted file mode 100644
index d57ab1e..0000000
--- a/example/components/ExternalLink.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Href, Link } from 'expo-router'
-import { openBrowserAsync } from 'expo-web-browser'
-import { type ComponentProps } from 'react'
-import { Platform } from 'react-native'
-
-type Props = Omit, 'href'> & { href: Href & string }
-
-export function ExternalLink({ href, ...rest }: Props) {
- return (
- {
- if (Platform.OS !== 'web') {
- // Prevent the default behavior of linking to the default browser on native.
- event.preventDefault()
- // Open the link in an in-app browser.
- await openBrowserAsync(href)
- }
- }}
- />
- )
-}
diff --git a/example/components/HapticTab.tsx b/example/components/HapticTab.tsx
deleted file mode 100644
index d7f0c3e..0000000
--- a/example/components/HapticTab.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs'
-import { PlatformPressable } from '@react-navigation/elements'
-import * as Haptics from 'expo-haptics'
-
-export function HapticTab(props: BottomTabBarButtonProps) {
- return (
- {
- if (process.env.EXPO_OS === 'ios') {
- // Add a soft haptic feedback when pressing down on the tabs.
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
- }
- props.onPressIn?.(ev)
- }}
- />
- )
-}
diff --git a/example/components/HelloWave.tsx b/example/components/HelloWave.tsx
deleted file mode 100644
index 377e1b3..0000000
--- a/example/components/HelloWave.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { useEffect } from 'react'
-import { StyleSheet } from 'react-native'
-import Animated, {
- useAnimatedStyle,
- useSharedValue,
- withRepeat,
- withSequence,
- withTiming,
-} from 'react-native-reanimated'
-
-import { ThemedText } from '@/components/ThemedText'
-
-export function HelloWave() {
- const rotationAnimation = useSharedValue(0)
-
- useEffect(() => {
- rotationAnimation.value = withRepeat(
- withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })),
- 4, // Run the animation 4 times
- )
- }, [rotationAnimation])
-
- const animatedStyle = useAnimatedStyle(() => ({
- transform: [{ rotate: `${rotationAnimation.value}deg` }],
- }))
-
- return (
-
- 👋
-
- )
-}
-
-const styles = StyleSheet.create({
- text: {
- fontSize: 28,
- lineHeight: 32,
- marginTop: -6,
- },
-})
diff --git a/example/components/ParallaxScrollView.tsx b/example/components/ParallaxScrollView.tsx
deleted file mode 100644
index 8283eb2..0000000
--- a/example/components/ParallaxScrollView.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import type { PropsWithChildren, ReactElement } from 'react'
-import { StyleSheet } from 'react-native'
-import Animated, {
- interpolate,
- useAnimatedRef,
- useAnimatedStyle,
- useScrollViewOffset,
-} from 'react-native-reanimated'
-
-import { ThemedView } from '@/components/ThemedView'
-import { useBottomTabOverflow } from '@/components/ui/TabBarBackground'
-import { useColorScheme } from '@/hooks/useColorScheme'
-
-const HEADER_HEIGHT = 250
-
-type Props = PropsWithChildren<{
- headerImage: ReactElement
- headerBackgroundColor: { dark: string; light: string }
-}>
-
-export default function ParallaxScrollView({
- children,
- headerImage,
- headerBackgroundColor,
-}: Props) {
- const colorScheme = useColorScheme() ?? 'light'
- const scrollRef = useAnimatedRef()
- const scrollOffset = useScrollViewOffset(scrollRef)
- const bottom = useBottomTabOverflow()
- const headerAnimatedStyle = useAnimatedStyle(() => {
- return {
- transform: [
- {
- translateY: interpolate(
- scrollOffset.value,
- [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
- [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75],
- ),
- },
- {
- scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
- },
- ],
- }
- })
-
- return (
-
-
-
- {headerImage}
-
- {children}
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- header: {
- height: HEADER_HEIGHT,
- overflow: 'hidden',
- },
- content: {
- flex: 1,
- padding: 32,
- gap: 16,
- overflow: 'hidden',
- },
-})
diff --git a/example/examples/AnimatedExample.tsx b/example/examples/AnimatedExample.tsx
index a2b0da9..8be0627 100644
--- a/example/examples/AnimatedExample.tsx
+++ b/example/examples/AnimatedExample.tsx
@@ -1,10 +1,4 @@
-import {
- HeroCarousel,
- interpolateInsideCarousel,
- useCarouselContext,
- useAutoCarouselSlideIndex,
- CarouselContextProvider,
-} from '@strv/react-native-hero-carousel'
+import { HeroCarousel, useInterpolateInsideCarousel } from '@strv/react-native-hero-carousel'
import { SafeAreaView, StyleSheet, View, Text, Dimensions } from 'react-native'
import { Image } from 'expo-image'
import { LinearGradient } from 'expo-linear-gradient'
@@ -24,17 +18,14 @@ const getRandomImageUrl = () => {
const images = Array.from({ length: 5 }, getRandomImageUrl)
const Slide = ({ image, title, index }: { image: string; title: string; index: number }) => {
- const { scrollValue } = useCarouselContext()
- const { index: slideIndex, total } = useAutoCarouselSlideIndex()
+ const progress = useInterpolateInsideCarousel({
+ valueBefore: 0,
+ thisValue: 1,
+ valueAfter: 0,
+ offset: 0.2,
+ })
const rStyle = useAnimatedStyle(() => {
- const progress = interpolateInsideCarousel(scrollValue.value, slideIndex, total, {
- valueBefore: 0,
- thisValue: 1,
- valueAfter: 0,
- offset: 0.2,
- })
-
return {
flex: 1,
width: '100%',
@@ -44,13 +35,13 @@ const Slide = ({ image, title, index }: { image: string; title: string; index: n
transformOrigin: 'center',
transform: [
{
- scale: interpolate(progress, [0, 1], [0.8, 1], Extrapolation.CLAMP),
+ scale: interpolate(progress.value, [0, 1], [0.8, 1], Extrapolation.CLAMP),
},
{
- rotate: `${interpolate(progress, [0, 1], [-15, 0], Extrapolation.CLAMP)}deg`,
+ rotate: `${interpolate(progress.value, [0, 1], [-15, 0], Extrapolation.CLAMP)}deg`,
},
],
- opacity: progress,
+ opacity: progress.value,
}
})
@@ -75,7 +66,7 @@ export default function AnimatedExample() {
}, [])
return (
-
+
@@ -88,7 +79,7 @@ export default function AnimatedExample() {
-
+
)
}
diff --git a/example/examples/BasicExample.tsx b/example/examples/BasicExample.tsx
index 2f31d9f..f7e0450 100644
--- a/example/examples/BasicExample.tsx
+++ b/example/examples/BasicExample.tsx
@@ -1,4 +1,4 @@
-import { HeroCarousel, CarouselContextProvider } from '@strv/react-native-hero-carousel'
+import { HeroCarousel } from '@strv/react-native-hero-carousel'
import { SafeAreaView, StyleSheet, View, Text, Dimensions } from 'react-native'
import { Image } from 'expo-image'
import { LinearGradient } from 'expo-linear-gradient'
@@ -38,7 +38,7 @@ export default function BasicExample() {
}, [])
return (
-
+
@@ -48,7 +48,7 @@ export default function BasicExample() {
-
+
)
}
diff --git a/example/examples/EnteringAnimationExample.tsx b/example/examples/EnteringAnimationExample.tsx
index 0f79e7e..38aa38b 100644
--- a/example/examples/EnteringAnimationExample.tsx
+++ b/example/examples/EnteringAnimationExample.tsx
@@ -1,8 +1,4 @@
-import {
- HeroCarousel,
- CarouselContextProvider,
- SlideAnimatedView,
-} from '@strv/react-native-hero-carousel'
+import { HeroCarousel } from '@strv/react-native-hero-carousel'
import { SafeAreaView, StyleSheet, View, Text, Dimensions } from 'react-native'
import { Image } from 'expo-image'
import { LinearGradient } from 'expo-linear-gradient'
@@ -36,17 +32,17 @@ const Slide = ({ image, title, index }: { image: string; title: string; index: n
-
+
{title}
-
-
+
Animation: {animationNames[index % animationNames.length]}
-
+
@@ -60,7 +56,7 @@ export default function EnteringAnimationExample() {
}, [])
return (
-
+
@@ -70,7 +66,7 @@ export default function EnteringAnimationExample() {
-
+
)
}
diff --git a/example/examples/TimerPaginationExample.tsx b/example/examples/TimerPaginationExample.tsx
index db2b349..0c632e5 100644
--- a/example/examples/TimerPaginationExample.tsx
+++ b/example/examples/TimerPaginationExample.tsx
@@ -1,8 +1,4 @@
-import {
- HeroCarousel,
- CarouselContextProvider,
- useAutoCarouselSlideIndex,
-} from '@strv/react-native-hero-carousel'
+import { HeroCarousel, useAutoCarouselSlideIndex } from '@strv/react-native-hero-carousel'
import { SafeAreaView, StyleSheet, View, Text, Dimensions } from 'react-native'
import { Image } from 'expo-image'
import { LinearGradient } from 'expo-linear-gradient'
@@ -63,7 +59,7 @@ export default function TimerPaginationExample() {
}
return (
-
+
@@ -80,7 +76,7 @@ export default function TimerPaginationExample() {
-
+
)
}
diff --git a/example/examples/VideoCarouselExample.tsx b/example/examples/VideoCarouselExample.tsx
index 31d57a9..ffe4bbf 100644
--- a/example/examples/VideoCarouselExample.tsx
+++ b/example/examples/VideoCarouselExample.tsx
@@ -1,12 +1,12 @@
import {
HeroCarousel,
- CarouselContextProvider,
useAutoCarouselSlideIndex,
+ useActiveItemEffect,
+ useIsActiveItem,
} from '@strv/react-native-hero-carousel'
import { SafeAreaView, StyleSheet, View, Text, Pressable, Dimensions, Platform } from 'react-native'
import { useVideoPlayer, VideoView } from 'expo-video'
import { LinearGradient } from 'expo-linear-gradient'
-import { useActiveSlideEffect, useIsActiveSlide } from '@/hooks/useActiveSlideEffect'
import { useEffect, useRef, useState } from 'react'
import { TimerPagination } from './components/TimerPagination'
import { useEvent, useEventListener } from 'expo'
@@ -30,9 +30,9 @@ const videoTitles = [
const Slide = ({ videoUri, title, index }: { videoUri: string; title: string; index: number }) => {
const player = useVideoPlayer(videoUri)
const { runAutoScroll } = useAutoCarouselSlideIndex()
- const isActiveSlide = useIsActiveSlide()
+ const isActiveSlide = useIsActiveItem()
const [duration, setDuration] = useState(0)
- useActiveSlideEffect(() => {
+ useActiveItemEffect(() => {
player.currentTime = 0
player.play()
return () => {
@@ -89,7 +89,7 @@ const Slide = ({ videoUri, title, index }: { videoUri: string; title: string; in
export default function VideoCarouselExample() {
return (
-
+
@@ -100,7 +100,7 @@ export default function VideoCarouselExample() {
-
+
)
}
diff --git a/example/examples/components/CarouselBase.tsx b/example/examples/components/CarouselBase.tsx
index dfbf1b3..59715b9 100644
--- a/example/examples/components/CarouselBase.tsx
+++ b/example/examples/components/CarouselBase.tsx
@@ -1,15 +1,11 @@
-import {
- HeroCarousel,
- HeroCarouselProps,
- CarouselContextProvider,
-} from '@strv/react-native-hero-carousel'
+import { HeroCarousel, HeroCarouselProps } from '@strv/react-native-hero-carousel'
import { SafeAreaView, StyleSheet, View } from 'react-native'
import { Stack } from 'expo-router'
import { Pagination } from '@/examples/components/Pagination'
export function CarouselBase({ children }: { children: HeroCarouselProps['children'] }) {
return (
-
+
@@ -17,7 +13,7 @@ export function CarouselBase({ children }: { children: HeroCarouselProps['childr
-
+
)
}
diff --git a/example/hooks/useActiveSlideEffect.ts b/example/hooks/useActiveSlideEffect.ts
deleted file mode 100644
index f9faf47..0000000
--- a/example/hooks/useActiveSlideEffect.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import {
- interpolateInsideCarousel,
- useAutoCarouselSlideIndex,
- useCarouselContext,
-} from '@strv/react-native-hero-carousel'
-import { useState } from 'react'
-import { runOnJS, useAnimatedReaction, useDerivedValue } from 'react-native-reanimated'
-
-export const useIsActiveSlide = () => {
- const { index, total } = useAutoCarouselSlideIndex()
- const { scrollValue } = useCarouselContext()
- const [isActive, setIsActive] = useState(false)
- useAnimatedReaction(
- () => scrollValue.value,
- (value) => {
- const result = interpolateInsideCarousel(value, index, total, {
- valueBefore: 0,
- thisValue: 1,
- valueAfter: 0,
- })
- if (result === 1) {
- runOnJS(setIsActive)(true)
- } else {
- runOnJS(setIsActive)(false)
- }
- },
- [index, total],
- )
- return isActive
-}
-
-export const useActiveSlideEffect = (
- effectFunc: () => () => void | undefined,
- deps: any[] = [],
-) => {
- const { index, total } = useAutoCarouselSlideIndex()
- const { scrollValue } = useCarouselContext()
- const value = useDerivedValue(() => {
- return interpolateInsideCarousel(scrollValue.value, index, total, {
- valueBefore: 0,
- thisValue: 1,
- valueAfter: 0,
- })
- })
-
- useAnimatedReaction(
- () => value.value,
- (value) => {
- if (value === 1) {
- runOnJS(effectFunc)()
- }
- },
- deps,
- )
-}
diff --git a/src/components/HeroCarousel/index.tsx b/src/components/HeroCarousel/index.tsx
index 17d14a4..8665fca 100644
--- a/src/components/HeroCarousel/index.tsx
+++ b/src/components/HeroCarousel/index.tsx
@@ -1,17 +1,18 @@
import React from 'react'
-import { useCarouselContext } from '../../context/CarouselContext'
+import { CarouselContextProvider, useCarouselContext } from '../../context/CarouselContext'
import { HeroCarouselSlide } from '../HeroCarouselSlide'
import { HeroCarouselAdapter } from '../AnimatedPagedView/Adapter'
import { useAutoScroll } from '../../hooks/useAutoScroll'
import { useInfiniteScroll } from '../../hooks/useInfiniteScroll'
import { DEFAULT_INTERVAL } from './index.preset'
+import { ItemAnimatedView } from '../ItemAnimatedView'
export type HeroCarouselProps = {
children: React.ReactNode[]
}
-export const HeroCarousel = ({ children }: HeroCarouselProps) => {
+const HeroCarousel = ({ children }: HeroCarouselProps) => {
const {
scrollValue,
userInteracted,
@@ -72,3 +73,9 @@ export const HeroCarousel = ({ children }: HeroCarouselProps) => {
>
)
}
+
+HeroCarousel.AnimatedView = ItemAnimatedView
+HeroCarousel.Provider = CarouselContextProvider
+HeroCarousel.Item = HeroCarouselSlide
+
+export { HeroCarousel }
diff --git a/src/components/HeroCarouselSlide/index.tsx b/src/components/HeroCarouselSlide/index.tsx
index 9277416..0a6d09c 100644
--- a/src/components/HeroCarouselSlide/index.tsx
+++ b/src/components/HeroCarouselSlide/index.tsx
@@ -1,5 +1,5 @@
import { View } from 'react-native'
-import { AutoCarouselSlideContext } from '../../context/SlideContext'
+import { ItemContext } from '../../context/ItemContext'
import { useAutoScroll } from '../../hooks/useAutoScroll'
import { useMemo } from 'react'
import { useManualScroll } from '../../hooks/useManualScroll'
@@ -21,14 +21,14 @@ export const HeroCarouselSlide = ({
}) => {
return (
- ({ index, total, runAutoScroll, goToPage }),
[index, total, runAutoScroll, goToPage],
)}
>
{children}
-
+
)
}
diff --git a/src/components/SlideAnimatedView/index.tsx b/src/components/ItemAnimatedView/index.tsx
similarity index 76%
rename from src/components/SlideAnimatedView/index.tsx
rename to src/components/ItemAnimatedView/index.tsx
index ef701b0..fae86d6 100644
--- a/src/components/SlideAnimatedView/index.tsx
+++ b/src/components/ItemAnimatedView/index.tsx
@@ -1,15 +1,14 @@
-import { useAutoCarouselSlideIndex, useCarouselContext } from '../../context'
import Animated, {
useDerivedValue,
useAnimatedReaction,
runOnJS,
AnimatedProps,
} from 'react-native-reanimated'
-import { interpolateInsideCarousel } from '../../utils'
import { useState } from 'react'
import { ViewProps } from 'react-native'
+import { useInterpolateInsideCarousel } from '../../hooks/useInterpolateInsideCarousel'
-type SlideAnimatedViewProps = {
+type ItemAnimatedViewProps = {
children: React.ReactNode
entering?: AnimatedProps['entering']
exiting?: AnimatedProps['exiting']
@@ -20,7 +19,7 @@ type SlideAnimatedViewProps = {
keepVisibleAfterExiting?: boolean
}
-export const SlideAnimatedView = ({
+export const ItemAnimatedView = ({
children,
entering,
exiting,
@@ -29,18 +28,17 @@ export const SlideAnimatedView = ({
exitingThreshold = 0.01,
keepVisibleAfterExiting = false,
style,
-}: SlideAnimatedViewProps) => {
- const { index, total } = useAutoCarouselSlideIndex()
- const { scrollValue } = useCarouselContext()
+}: ItemAnimatedViewProps) => {
+ const progress = useInterpolateInsideCarousel({
+ valueBefore: 0,
+ thisValue: 1,
+ valueAfter: 0,
+ })
const [shouldShow, setShouldShow] = useState(false)
const value = useDerivedValue(() => {
- return interpolateInsideCarousel(scrollValue.value, index, total, {
- valueBefore: 0,
- thisValue: 1,
- valueAfter: 0,
- })
- }, [index, total, scrollValue])
+ return progress.value
+ }, [progress])
// Track when value becomes 1 to trigger entering animation
useAnimatedReaction(
diff --git a/src/components/index.ts b/src/components/index.ts
index cd28e97..be36b76 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -1,2 +1 @@
-export * from './HeroCarousel'
-export * from './SlideAnimatedView'
+export { HeroCarousel, HeroCarouselProps } from './HeroCarousel'
diff --git a/src/context/SlideContext/index.tsx b/src/context/ItemContext/index.tsx
similarity index 82%
rename from src/context/SlideContext/index.tsx
rename to src/context/ItemContext/index.tsx
index c09bc39..623865c 100644
--- a/src/context/SlideContext/index.tsx
+++ b/src/context/ItemContext/index.tsx
@@ -2,7 +2,7 @@ import { createContext, useContext } from 'react'
import { useAutoScroll } from '../../hooks/useAutoScroll'
import { useManualScroll } from '../../hooks/useManualScroll'
-export const AutoCarouselSlideContext = createContext<{
+export const ItemContext = createContext<{
index: number
total: number
runAutoScroll: ReturnType['runAutoScroll']
@@ -10,7 +10,7 @@ export const AutoCarouselSlideContext = createContext<{
} | null>(null)
export const useAutoCarouselSlideIndex = () => {
- const context = useContext(AutoCarouselSlideContext)
+ const context = useContext(ItemContext)
if (!context) {
throw new Error('useAutoCarouselSlideIndex must be used within a AutoCarouselSlide')
}
diff --git a/src/context/index.tsx b/src/context/index.tsx
index 93a6354..1d12d3c 100644
--- a/src/context/index.tsx
+++ b/src/context/index.tsx
@@ -1,2 +1,2 @@
-export * from './CarouselContext'
-export * from './SlideContext'
+export { useCarouselContext } from './CarouselContext'
+export * from './ItemContext'
diff --git a/src/hooks/index.tsx b/src/hooks/index.tsx
new file mode 100644
index 0000000..921c2be
--- /dev/null
+++ b/src/hooks/index.tsx
@@ -0,0 +1,2 @@
+export { useInterpolateInsideCarousel } from './useInterpolateInsideCarousel'
+export * from './useActiveItemEffect'
diff --git a/src/hooks/useActiveItemEffect.ts b/src/hooks/useActiveItemEffect.ts
new file mode 100644
index 0000000..572bb4a
--- /dev/null
+++ b/src/hooks/useActiveItemEffect.ts
@@ -0,0 +1,42 @@
+import { useInterpolateInsideCarousel } from './useInterpolateInsideCarousel'
+import { useState } from 'react'
+import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'
+
+export const useIsActiveItem = () => {
+ const progress = useInterpolateInsideCarousel({
+ valueBefore: 0,
+ thisValue: 1,
+ valueAfter: 0,
+ })
+ const [isActive, setIsActive] = useState(false)
+ useAnimatedReaction(
+ () => progress.value,
+ (value) => {
+ if (value === 1) {
+ runOnJS(setIsActive)(true)
+ } else {
+ runOnJS(setIsActive)(false)
+ }
+ },
+ [],
+ )
+ return isActive
+}
+
+export const useActiveItemEffect = (effectFunc: () => () => void | undefined, deps: any[] = []) => {
+ const progress = useInterpolateInsideCarousel({
+ valueBefore: 0,
+ thisValue: 1,
+ valueAfter: 0,
+ })
+
+ useAnimatedReaction(
+ () => progress.value,
+ (value) => {
+ if (value === 1) {
+ runOnJS(effectFunc)()
+ }
+ },
+ deps,
+ )
+}
diff --git a/src/hooks/useInterpolateInsideCarousel.ts b/src/hooks/useInterpolateInsideCarousel.ts
new file mode 100644
index 0000000..5fe154b
--- /dev/null
+++ b/src/hooks/useInterpolateInsideCarousel.ts
@@ -0,0 +1,16 @@
+import { useAutoCarouselSlideIndex, useCarouselContext } from '../context'
+import { interpolateInsideCarousel } from '../utils'
+import { useDerivedValue } from 'react-native-reanimated'
+
+export const useInterpolateInsideCarousel = (values: {
+ valueBefore: number
+ thisValue: number
+ valueAfter: number
+ offset?: number
+}) => {
+ const { scrollValue } = useCarouselContext()
+ const { index, total } = useAutoCarouselSlideIndex()
+ return useDerivedValue(() => {
+ return interpolateInsideCarousel(scrollValue.value, index, total, values)
+ }, [scrollValue, values])
+}
diff --git a/src/index.ts b/src/index.ts
index 1710fbf..dd9fb23 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,4 @@
export * from './components'
export * from './context'
export * from './utils'
+export * from './hooks'
diff --git a/src/utils/PausableTimeout.ts b/src/utils/PausableTimeout.ts
index 9c9641e..939e681 100644
--- a/src/utils/PausableTimeout.ts
+++ b/src/utils/PausableTimeout.ts
@@ -20,7 +20,7 @@ export class PausableTimeout {
callbackStartTime: number = 0
remaining: number = 0
paused: boolean = false
- timerId: NodeJS.Timeout | null = null
+ timerId: ReturnType | null = null
onPause?: (remaining: number) => void = () => {}
onResume?: (remaining: number) => void = () => {}
_callback: () => void