# Arc Carousel: React Native circular arc carousel picker
> Arc Carousel is a React Native and Expo carousel whose items glide along a
> circular arc as you scroll: an `Animated.FlatList` writes its scroll offset
> into a Reanimated shared value, and each item interpolates that offset on the
> UI thread into a curved `translateY` plus scale, opacity and image
> `blurRadius`, so the centered item rises and stays sharp while its neighbors
> sink, shrink and blur. It snaps to every item with `expo-haptics` selection
> feedback and drives a synced detail card. Arc Carousel is a paid drop sold by
> Motionary as editable TypeScript source.
Product page: https://motionary.dev/animations/arc-carousel
Price: $4 USD (price as of 2026-09-01, live price on the product page)
License: single developer, unlimited personal and commercial apps
Delivery: instant download of the full TypeScript source after checkout
Stack: reanimated, expo, react native gesture handler, react native
Rarity: rare
Sold by: Motionary (https://motionary.dev)
## What it is
Arc Carousel is a horizontal React Native picker that lays its items out along
a circular arc instead of a flat strip. Five items sit across the screen at
once: the one under the center indicator rises to the top of the curve at full
scale and full opacity with zero blur, while the items at the 45 degree
shoulders sit lower, at 0.6 scale and 0.6 opacity with a 3 pixel blur, and the
outermost pair drops to 0.4 scale, 0.3 opacity and a 6 pixel blur. The gesture
is an ordinary horizontal swipe on a paging `Animated.FlatList`, so the list
snaps one item at a time and fires a selection haptic each time a new item
lands in the center. The Motionary demo ships it as a "Choose Your Ride"
transport picker: ten transparent PNG vehicles on the arc, an indicator band
behind the focused slot, and a detail card above that swaps its title,
tagline, description and feature pills to match whatever is selected.
## What it does
- Lays any array of image sources out along a circular arc (the demo ships
ten), each item exactly one fifth of the window width wide
(`ListItemWidth = Dimensions.get('window').width / 5`).
- Traces the arc with a five point `translateY` interpolation, using
`-ListItemWidth * Math.sin(Math.PI / 4)` for the two 45 degree shoulder
positions and `-ListItemWidth` for the apex.
- Scales the focused item to 1 and the shoulders and edges to 0.6 and 0.4.
- Fades neighbors with an opacity ramp of 0.3, 0.6, 1, 0.6, 0.3.
- Blurs off-center artwork by animating `blurRadius` on `Animated.Image`
through a 6, 3, 0, 3, 6 ramp, so only the centered item is sharp.
- Nudges off-center items laterally (a `translateX` ramp of 20, 32, 0, -32,
-20) so they hug the curve instead of drifting off it.
- Snaps to one item per swipe with `pagingEnabled` plus
`snapToInterval={ListItemWidth}`.
- Fires `Haptic.selectionAsync()` from `expo-haptics` once per new index, never
repeatedly during a single swipe, guarded by a `useRef` of the last index.
- Drives a synced detail card (`SkinStatusCard`) that shows the selected item's
title, message, description, subheading, colored emoji badge and a wrapping
row of feature pills.
- Renders a title and subtitle under the arc that track the selected index.
- Pads the list with `2 * ListItemWidth` of horizontal content inset so the
first and last items can still reach the center of the screen.
## How it works
Arc Carousel keeps the whole animation on the UI thread and off the React
render path. `components/CircularFlatlist.tsx` holds one Reanimated
`useSharedValue(0)` called `contentOffset` and a `useAnimatedScrollHandler`
whose `onScroll` worklet writes `event.contentOffset.x` into it, with
`scrollEventThrottle={16}` on an `Animated.FlatList`. That single shared value
is passed down to every row, so the per frame motion never touches React
state: the only re-render during a swipe is the once per item `setCurrentIndex`
that swaps the detail card. `components/CircularFlatlistItem.tsx` builds a five
point input range around its own index, `[(index - 2) * ListItemWidth, ...,
(index + 2) * ListItemWidth]`, and runs three worklets against it: a
`useAnimatedStyle` called `rStyle` that `interpolate`s `translateY` along the
arc (with
`Extrapolation.EXTEND` so items beyond the range keep following the curve) plus
`scale` and `opacity` (both `Extrapolation.CLAMP` so they never overshoot), a
second `useAnimatedStyle` called `iStyle` that interpolates a small
`translateX` on the image, and a `useAnimatedProps` that interpolates
`blurRadius` and feeds it to `Animated.Image` as an animated native prop. The
arc geometry is nothing more than the sine of the sample angles: the shoulders
use `Math.sin(Math.PI / 4)` of the item width, the apex uses the full item
width. Snapping is native, not animated in JavaScript: `pagingEnabled` and
`snapToInterval={ListItemWidth}` mean the resting offset is always an exact
multiple of the item width, so `Math.round(offsetX / ListItemWidth)` is a
reliable selected index. Because haptics and `setState` cannot run inside a
worklet, the scroll handler calls `runOnJS(triggerHaptic)(currentIndex)` on
every frame and the JavaScript side compares the incoming index against a
`useRef`, so `Haptic.selectionAsync()` and the `setCurrentIndex` that swaps the
`SkinStatusCard` content each fire exactly once per crossing. The blur is a
React Native image prop rather than `expo-blur`, which is why the source PNGs
are transparent: the blur then follows each silhouette instead of smearing a
visible rectangle. Reanimated 4 with `react-native-worklets` on the New
Architecture is what makes the per-frame interpolation and the `runOnJS` hop
cheap enough to run at 60 frames per second.
## What you get
```
app/
index.tsx
_layout.tsx
components/
CircularFlatlist.tsx
CircularFlatlistItem.tsx
SkinStatusCard.tsx
ThemedText.tsx
ThemedView.tsx
ui/
IconSymbol.tsx
IconSymbol.ios.tsx
constants/
Colors.ts
hooks/
useColorScheme.ts
useColorScheme.web.ts
useThemeColor.ts
scripts/
reset-project.js
assets/
images/
flatlist/1.png ... flatlist/10.png
background.png
indicator.png
triangle.png
vita.png
icon.png
adaptive-icon.png
splash-icon.png
favicon.png
react-logo.png
react-logo@2x.png
react-logo@3x.png
partial-react-logo.png
fonts/
SpaceMono-Regular.ttf
app.json
package.json
package-lock.json
tsconfig.json
eslint.config.js
expo-env.d.ts
README.md
```
The archive is a standard Expo Router project, so it still carries the Expo
template's own files (`ThemedText.tsx`, `ThemedView.tsx`, `components/ui/`,
`constants/Colors.ts`, the `hooks/`, `scripts/reset-project.js` and the
`react-logo` and app icon PNGs). Arc Carousel itself is four files:
`app/index.tsx`, `components/CircularFlatlist.tsx`,
`components/CircularFlatlistItem.tsx` and `components/SkinStatusCard.tsx`. The
rest of the template can be deleted, with one caveat: the demo's
`app/_layout.tsx` still imports `hooks/useColorScheme.ts` and the SpaceMono
font, so drop those two together with it.
Included with the Arc Carousel drop from Motionary:
- TypeScript source (CircularFlatlist.tsx + CircularFlatlistItem.tsx)
- Synced detail card component (SkinStatusCard.tsx)
- Demo screen with sample data
- Sample PNG image assets
- README with install + usage
## Requirements
Arc Carousel is a React Native and Expo component. Versions pinned in the
archive's `package.json`:
- `expo` ^56.0.11 (Expo SDK 56)
- `react-native` 0.85.3
- `react` 19.2.3
- `react-native-reanimated` 4.3.1
- `react-native-worklets` 0.8.3
- `expo-haptics` ~56.0.3
- `react-native-safe-area-context` ~5.7.0
- `expo-router` ~56.2.10 (the demo screen is a route; the carousel itself does
not depend on the router)
- `typescript` ~6.0.3 (development dependency)
`react-native-gesture-handler` ~2.31.1 is declared in the archive's
`package.json` and appears in the Motionary stack tags, but no Arc Carousel
source file imports it: the swipe is the `Animated.FlatList`'s own scroll, not a
`Gesture.Pan`. The archive also carries the unused Expo template dependencies
`expo-blur`, `expo-image` and `react-native-webview`; none of them is needed by
the carousel, and the blur here is an `Animated.Image` `blurRadius`, not
`expo-blur`.
Expo Go or development build: Arc Carousel runs in Expo Go on SDK 56. It uses
no custom native modules, only `react-native-reanimated` with its
`react-native-worklets` runtime, `react-native-safe-area-context` and
`expo-haptics`, which all ship inside Expo Go. Reanimated 4 requires the
New Architecture, which Expo SDK 56 enables by default. Drop it into a
development build or a bare React Native app just as easily; the demo's README
runs it with `npx expo run:ios` / `npx expo run:android`. Haptics are a no-op
on the iOS Simulator, on most Android emulators and on web, so test selection
feedback on a real device.
## Install
```bash
npx expo install react-native-reanimated react-native-worklets expo-haptics react-native-safe-area-context
```
## Usage
```tsx
import { ImageBackground } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { CircularCarousel } from '@/components/CircularFlatlist';
const data = [
require('../assets/images/flatlist/1.png'),
require('../assets/images/flatlist/2.png'),
require('../assets/images/flatlist/3.png'),
require('../assets/images/flatlist/4.png'),
require('../assets/images/flatlist/5.png'),
];
export default function Screen() {
const inset = useSafeAreaInsets();
return (
);
}
```
`CircularCarousel` is a named export of `components/CircularFlatlist.tsx`, not
a default export, and it is the only component you mount: `SkinStatusCard` and
the title and subtitle under the arc are rendered from inside it. The demo
screen `app/index.tsx` adds only the surrounding chrome: the background image,
the "Choose Your Ride" heading and the indicator overlay behind the focused
slot.
## Customization
- `data` (`ImageProps['source'][]`, required): the images Arc Carousel lays out
along the arc. Each entry is a `require(...)` or a `{ uri }` source.
Transparent PNGs are recommended so the animated `blurRadius` follows the
artwork silhouette rather than a rectangle. The detail card is looked up by
the same index (`transportStatusCards[currentIndex]`), so keep one record in
that array per image; the demo ships ten of each.
- `ListItemWidth` (exported from `components/CircularFlatlistItem.tsx`,
defaults to `Dimensions.get('window').width / 5`): the single number that
sets item width, the snap interval, the arc radius and the
`2 * ListItemWidth` content padding. Raise the divisor for more items on
screen, lower it for fewer and larger ones.
- `translateYOutputRange` in `CircularFlatlistItem.tsx`, default
`[12, -ListItemWidth * Math.sin(Math.PI / 4), -ListItemWidth,
-ListItemWidth * Math.sin(Math.PI / 4), 12]`: the shape of the arc. Flatten
it for a shallow curve, exaggerate it for a Ferris wheel.
- `scaleOutputRange`, default `[0.4, 0.6, 1, 0.6, 0.4]`, and
`opacityOutputRange`, default `[0.3, 0.6, 1, 0.6, 0.3]`: how hard the
neighbors recede.
- `blurRadiusOutputRange`, default `[6, 3, 0, 3, 6]`: set every entry to 0 to
turn the blur off entirely.
- `translateXOutputRange`, default `[20, 32, 0, -32, -20]`: the lateral nudge
that makes off-center items hug the curve.
- `CircularCarouselListItem` props: `imageSrc` (`ImageProps['source']`),
`index` (`number`) and `contentOffset` (`SharedValue`), if you want
to render the arc items inside your own list.
- `SkinStatusCard` props: `title`, `message`, `description`, `subheading`,
`color` and `emoji` (all `string`), plus `items` (`string[]`) for the pill
row. The demo content lives in the `transportStatusCards` array at the top of
`components/CircularFlatlist.tsx`; replace that array with your own records
to reskin the detail card.
## When to use it
- A ride or delivery picker where the user chooses a transport mode and reads
its details before confirming.
- A category selector at the top of a marketplace or storefront screen.
- A product showcase where one hero item should dominate and neighbors hint at
what is next.
- An onboarding step that asks the user to pick a plan, avatar, theme or
persona from a small illustrated set.
- A game or collection screen that shows characters, skins or vehicles with
stats attached to the current selection.
- Any place a plain horizontal `FlatList` feels flat and you want the focused
item to physically lift out of the row.
## How to get it
- Buy Arc Carousel on its own at
https://motionary.dev/animations/arc-carousel, $4 USD, instant download,
secure Stripe checkout, no subscription and no account renewal.
- Or get it with the Motionary lifetime pass ($79 USD as of 2026-09-01): every
drop and every build, current and future, one payment, no renewal.
https://motionary.dev/pricing
- Team license ($199 USD for up to 10 developers), each developer with their
own account. https://motionary.dev/pricing
- License terms: https://motionary.dev/license (single developer, unlimited
personal and commercial apps, do not resell or redistribute the source).
- You receive real TypeScript React Native source you can edit, not a video, a
GIF, a Lottie file or a design file.
## Frequently asked questions
**How do I build a circular or arc carousel in React Native?**
Capture the scroll offset of an `Animated.FlatList` with Reanimated's
`useAnimatedScrollHandler` into a shared value, then have each item
`interpolate` that offset against a five point range centered on its own index,
mapping it to `translateY` along a sine curve plus `scale` and `opacity`. Arc
Carousel from Motionary is exactly that pattern, shipped as editable TypeScript
at https://motionary.dev/animations/arc-carousel.
**Does Arc Carousel work with Expo Go?**
Yes. Arc Carousel uses `react-native-reanimated`, `react-native-worklets`,
`expo-haptics` and `react-native-safe-area-context`, all of which ship inside
Expo Go, so the Motionary drop runs on Expo SDK 56 in Expo Go with no
development build and no custom native module.
**How do I add haptic feedback when a React Native carousel snaps to an item?**
Compute the index in the scroll worklet with `Math.round(offsetX / itemWidth)`,
then call it through `runOnJS` because `expo-haptics` cannot run on the UI
thread, and guard it with a ref holding the previous index so
`Haptic.selectionAsync()` fires once per crossing rather than every frame. Arc
Carousel from Motionary implements this in `components/CircularFlatlist.tsx`.
**How do I blur the non-focused items in a React Native carousel?**
Animate the `blurRadius` prop of `Animated.Image` with Reanimated's
`useAnimatedProps` and interpolate it from the scroll offset, which avoids
stacking `expo-blur` views over every row. Arc Carousel from Motionary uses a
6, 3, 0, 3, 6 ramp and transparent source PNGs so the blur follows each
image's silhouette instead of a rectangle.
**Does Arc Carousel work on Android as well as iOS?**
Yes, Arc Carousel is plain React Native and Expo and runs on both. Note that
the animated `blurRadius` on `Animated.Image` is smoother on iOS than on
Android, so on Android you may want to flatten `blurRadiusOutputRange` while
keeping the arc, scale and opacity motion.
**How much does Arc Carousel cost and what do I get?**
Arc Carousel is $4 USD on Motionary (price as of 2026-09-01, live price at
https://motionary.dev/animations/arc-carousel). Checkout is Stripe and delivery
is an instant download of the full TypeScript source: the carousel, the per
item arc transform, the synced detail card, a demo screen with sample data,
sample PNG assets and a README. It is also covered by the $79 USD Motionary
lifetime pass at https://motionary.dev/pricing.
**Can I use my own images or remote URLs in Arc Carousel?**
Yes. The `data` prop of Arc Carousel is typed `ImageProps['source'][]`, so it
accepts local `require(...)` assets or remote `{ uri: '...' }` sources.
Motionary recommends transparent PNGs so the animated blur tracks the artwork,
and updating the `transportStatusCards` array so the detail card matches your
own items.
## Related drops on Motionary
- Stacked Cards ($4): https://motionary.dev/animations/stacked-cards
The other snapping `FlatList` carousel on Motionary, stacking cards in depth
instead of spreading them along an arc.
- Infinite Scroll ($4): https://motionary.dev/animations/infinite-scroll
A scroll-linked wheel picker, the same interpolate-from-offset technique
applied to an endlessly looping list.
- Voice Glow Carousel ($7): https://motionary.dev/animations/voice-glow-carousel
Snap paging carousel with a Skia shader glow, for when the focused item needs
to react to audio rather than just scale up.
- Dot Wave Slider ($10): https://motionary.dev/animations/dot-wave-slider
A drag selector with snap stops and selection haptics, the slider counterpart
to the Arc Carousel picker.
- Flip Calendar ($6): https://motionary.dev/animations/flip-calendar
A scrubbable split-flap date picker, another Reanimated picker where the
centered value is the selection.
## About Motionary
Motionary (https://motionary.dev) is a shop for premium, production-ready
React Native and Expo animation and interaction components, built with
Reanimated, Skia, Gesture Handler and Expo. Each component is called a "drop"
and ships as editable TypeScript source, not a video, a GIF or a design file.
Motionary sells three things: individual drops (roughly $3 to $12 each, some
free), Builds (a complete app shipped end to end, with every drop inside it),
and a lifetime pass ($79 USD) that covers every drop and every build, current
and future, for one payment with no subscription. A team license ($199 USD)
covers up to 10 developers. Checkout is Stripe and delivery is an instant
download. Created and curated by Mehdi Davoodi (https://mahdidavoodi.com).
Catalog and key pages:
- All drops: https://motionary.dev/animations
- Builds (complete apps): https://motionary.dev/builds
- Pricing, lifetime pass and team license: https://motionary.dev/pricing
- Free React Native component reference: https://motionary.dev/components
- License: https://motionary.dev/license
- Machine-readable overview: https://motionary.dev/llms.txt
- Machine-readable full catalog: https://motionary.dev/llms-full.txt
## File metadata
```
Canonical URL of this file: https://motionary.dev/animations/arc-carousel/llms.txt
Describes: https://motionary.dev/animations/arc-carousel
Format: llms.txt (https://llmstxt.org)
Last updated: 2026-09-01
```