Utility library for React Native Reanimated and Gesture Handler providing mathematical functions, animations, transformations, and helper utilities for building complex gesture-driven animations.
—
Additional utility functions for physics calculations and array manipulations that support common animation and interaction patterns.
Calculate the optimal snap point based on gesture velocity and current position.
/**
* Select snap point based on value and velocity
* @param value - Current position/value
* @param velocity - Current velocity
* @param points - Array of possible snap points
* @returns The optimal snap point
*/
function snapPoint(
value: number,
velocity: number,
points: ReadonlyArray<number>
): number;Usage Example:
import { snapPoint } from "react-native-redash";
import {
useSharedValue,
useAnimatedGestureHandler,
withSpring
} from "react-native-reanimated";
import { PanGestureHandler } from "react-native-gesture-handler";
export const SnapScrollView = () => {
const translateX = useSharedValue(0);
// Define snap points (e.g., for horizontal card stack)
const snapPoints = [0, -150, -300, -450];
const gestureHandler = useAnimatedGestureHandler({
onStart: () => {
// Store initial position if needed
},
onActive: (event) => {
translateX.value = event.translationX;
},
onEnd: (event) => {
// Find optimal snap point based on position and velocity
const destination = snapPoint(
translateX.value,
event.velocityX,
snapPoints
);
// Animate to snap point
translateX.value = withSpring(destination, {
damping: 15,
stiffness: 100
});
}
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }]
}));
return (
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View style={[styles.scrollContainer, animatedStyle]}>
{/* Scrollable content */}
</Animated.View>
</PanGestureHandler>
);
};Advanced Snap Point Example:
import { snapPoint } from "react-native-redash";
export const VerticalSnapCarousel = () => {
const translateY = useSharedValue(0);
const cardHeight = 200;
const cardSpacing = 20;
// Generate snap points for vertical card stack
const snapPoints = Array.from({ length: 5 }, (_, i) =>
-(i * (cardHeight + cardSpacing))
);
const gestureHandler = useAnimatedGestureHandler({
onEnd: (event) => {
const currentPosition = translateY.value;
const velocity = event.velocityY;
// Factor in velocity for more natural snapping
const destination = snapPoint(currentPosition, velocity, snapPoints);
translateY.value = withSpring(destination, {
damping: 20,
stiffness: 200,
mass: 1
});
}
});
return (
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View style={animatedStyle}>
{/* Cards */}
</Animated.View>
</PanGestureHandler>
);
};Move elements within an array while maintaining proper indices.
/**
* Move array element from one index to another
* @param input - Input array to modify
* @param from - Source index
* @param to - Destination index
* @returns New array with moved element
*/
function move<T>(input: T[], from: number, to: number): T[];Usage Example:
import { move } from "react-native-redash";
import { useSharedValue, runOnJS } from "react-native-reanimated";
export const ReorderableList = () => {
const [items, setItems] = useState(['A', 'B', 'C', 'D', 'E']);
const draggedIndex = useSharedValue(-1);
const moveItem = (fromIndex: number, toIndex: number) => {
setItems(current => move(current, fromIndex, toIndex));
};
const gestureHandler = useAnimatedGestureHandler({
onStart: (event, context) => {
// Determine which item is being dragged
const index = Math.floor(event.y / ITEM_HEIGHT);
draggedIndex.value = index;
context.startIndex = index;
},
onActive: (event, context) => {
const currentIndex = Math.floor(event.y / ITEM_HEIGHT);
if (currentIndex !== context.startIndex) {
// Move item in array
runOnJS(moveItem)(context.startIndex, currentIndex);
context.startIndex = currentIndex;
}
},
onEnd: () => {
draggedIndex.value = -1;
}
});
return (
<PanGestureHandler onGestureEvent={gestureHandler}>
<View>
{items.map((item, index) => (
<ReorderableItem
key={item}
item={item}
index={index}
isDragged={draggedIndex.value === index}
/>
))}
</View>
</PanGestureHandler>
);
};Animated List Reordering:
import { move } from "react-native-redash";
import {
useSharedValue,
useAnimatedStyle,
withSpring,
runOnJS
} from "react-native-reanimated";
export const AnimatedReorderableList = () => {
const [data, setData] = useState([
{ id: '1', title: 'Item 1' },
{ id: '2', title: 'Item 2' },
{ id: '3', title: 'Item 3' },
{ id: '4', title: 'Item 4' }
]);
const positions = useSharedValue(
data.map((_, index) => index)
);
const reorderItems = (from: number, to: number) => {
// Update both data and positions
setData(current => move(current, from, to));
positions.value = move(positions.value, from, to);
};
const createGestureHandler = (index: number) =>
useAnimatedGestureHandler({
onActive: (event) => {
const newIndex = Math.floor(event.absoluteY / ITEM_HEIGHT);
if (newIndex !== index && newIndex >= 0 && newIndex < data.length) {
runOnJS(reorderItems)(index, newIndex);
}
}
});
return (
<View>
{data.map((item, index) => {
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateY: withSpring(positions.value[index] * ITEM_HEIGHT) }
]
}));
return (
<PanGestureHandler
key={item.id}
onGestureEvent={createGestureHandler(index)}
>
<Animated.View style={[styles.item, animatedStyle]}>
<Text>{item.title}</Text>
</Animated.View>
</PanGestureHandler>
);
})}
</View>
);
};import { snapPoint, move } from "react-native-redash";
export const SnapReorderCarousel = () => {
const [items, setItems] = useState(['Card 1', 'Card 2', 'Card 3', 'Card 4']);
const translateX = useSharedValue(0);
const activeIndex = useSharedValue(0);
const cardWidth = 250;
const cardSpacing = 20;
// Calculate snap points for each card
const snapPoints = items.map((_, index) =>
-(index * (cardWidth + cardSpacing))
);
const gestureHandler = useAnimatedGestureHandler({
onActive: (event) => {
translateX.value = event.translationX;
// Calculate which card is currently in focus
const currentIndex = Math.round(-translateX.value / (cardWidth + cardSpacing));
activeIndex.value = Math.max(0, Math.min(items.length - 1, currentIndex));
},
onEnd: (event) => {
// Snap to nearest card
const destination = snapPoint(
translateX.value,
event.velocityX,
snapPoints
);
translateX.value = withSpring(destination);
// Update active index based on final position
const finalIndex = Math.round(-destination / (cardWidth + cardSpacing));
activeIndex.value = finalIndex;
}
});
// Double tap to move card to front
const doubleTapHandler = useAnimatedGestureHandler({
onEnd: () => {
const currentIndex = activeIndex.value;
if (currentIndex > 0) {
runOnJS(setItems)(current => move(current, currentIndex, 0));
translateX.value = withSpring(0);
activeIndex.value = 0;
}
}
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }]
}));
return (
<PanGestureHandler onGestureEvent={gestureHandler}>
<TapGestureHandler numberOfTaps={2} onGestureEvent={doubleTapHandler}>
<Animated.View style={[styles.carousel, animatedStyle]}>
{items.map((item, index) => (
<View key={item} style={styles.card}>
<Text>{item}</Text>
</View>
))}
</Animated.View>
</TapGestureHandler>
</PanGestureHandler>
);
};Utility Function Behavior:
snapPoint: Uses velocity and position to predict where the user intends to end upmove: Handles negative indices by wrapping around the array lengthsnapPoint considers momentum (velocity * 0.2) when calculating the target destinationmove creates a new array and handles edge cases like moving beyond array boundsInstall with Tessl CLI
npx tessl i tessl/npm-react-native-redash