# Attachment Sheet: React Native attachment bottom sheet for Expo chat > Attachment Sheet is a React Native and Expo component sold by Motionary in > which the paperclip button of a chat input bar and the expanded attachment > sheet are the same view: one Reanimated shared value interpolates left, top, > width, height and all four corner radii from the button frame to the sheet > frame, while a react-native-gesture-handler pan drags the sheet between > detents or flicks it away. Attachment Sheet ships eight cross-faded panes > behind a real native UISegmentedControl and is pixel-matched to Telegram. Product page: https://motionary.dev/animations/attachment-sheet Price: $12 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: expo, react native, reanimated Rarity: legendary Sold by: Motionary (https://motionary.dev) ## What it is Attachment Sheet is a React Native and Expo recreation of Telegram's attachment menu, sold as editable TypeScript source by Motionary. You tap the paperclip button at the left of the chat input bar and it grows, in place, into a bottom sheet: the same rounded rectangle widens, climbs and squares off its corners until it is the sheet, with the wallpaper behind it dimming on its own curve. Once open, you drag the sheet's header to pull it up to a full screen detent, or push it down past a threshold to dismiss it, and you switch between eight attachment panes (gallery, text, file, location, poll, checklist, audio, contact) from a floating liquid glass bar at the bottom. Every spring, inset, radius and color in Attachment Sheet was measured frame by frame from a screen recording of the real Telegram interaction. ## What it does - Morphs the 40pt circular paperclip button into the full sheet as one view, never a crossfade between two components, so there is no popping frame. - Drives the entire morph from a single Reanimated `progress` shared value that interpolates `left`, `top`, `width`, `height` and the four corner radii. - Runs the backdrop dim on its own `withTiming` curve (420ms open, 260ms close, to a flat black at 28% opacity) rather than on the sheet's spring. - Pans the sheet between two detents with a `Gesture.Pan`: dragging up grows the sheet because the bottom edge is pinned, dragging down slides the whole sheet away at fixed height so it reads as a dismissal, not a shrink. - Applies `overshootResistance` (0.35) past the expanded detent, and dismisses on a downward flick above 900 velocity or past 110pt of travel. - Widens the sheet out to both display edges as it expands, driven by an `expansion` derived value, and opens its bottom corners wider (54) than its top ones (38) so they sit inside the display's own radius. - Ships eight panes (gallery, text, file, location, poll, checklist, audio, contact), stacked absolutely and cross-faded, so each pane keeps its scroll position and its typed text across a switch. - Renders the segment bar as a real UIKit `UISegmentedControl` inside a bundled Swift Expo module, so the selection indicator and its transition are the system's, and the control scrolls its own overflowing segments. - Answers a segment tap by dipping the whole sheet 10pt and springing it back, which is the movement the reference recording shows. - Recycles a three column photo grid on Legend List with `expo-image` cells (memory and disk cached, 140ms transition, tap to toggle a checkmark), filled with sample photos in `photos.ts` for you to swap for a real media library. - Fires `expo-haptics` impact on open and selection feedback on segment change. - Falls back to a white wash where liquid glass is unavailable (checked with `isLiquidGlassAvailable()`), so the menu bar and the circular header buttons still read correctly. - Includes a full chat mock (gradient wallpaper, header, group info bubble, input bar) so Attachment Sheet can be demoed in context. ## How it works The whole trick in Attachment Sheet is that the paperclip button and the sheet are one `Animated.View`, not two components handing off to each other. `components/attachment/AttachmentSheet.tsx` keeps a `progress` shared value and a `useAnimatedStyle` that runs `interpolate(progress, [0, 1], ...)` over the frame's `left`, `top`, `width`, `height` and all four border radii, from the measured button frame returned by `getButtonFrame()` to the sheet frame returned by `getSheetFrame()`. The sheet's contents are laid out once at full sheet width and then uniformly scaled by a `contentScale` derived value with `transformOrigin: 'top left'`, so nothing reflows mid flight and the only work per frame on the UI thread is a scale plus a frame interpolation; widening the contents by layout instead would recolumn the photo grid on every frame of the drag. Dragging is a `Gesture.Pan` built per attachment point by `createDrag`, gated with `.activeOffsetY([-8, 8])` and `.failOffsetX([-16, 16])` so header buttons still take taps, and it writes `sheetTop` directly; two `useDerivedValue` hooks then separate the two meanings of that number, `openHeight` (grew taller, because the bottom edge is pinned) and `expansion` (interpolated and clamped between the resting and expanded detents). On release the gesture picks a detent from position and velocity and snaps with `withSpring(..., springs.detent)`, or calls `close` through `runOnJS`. The floating menu bar has to hold still while everything else grows, so `menuBarStyle` counter-scales it by `1 / expandScale` and translates back the ground the sheet's left edge gave up, both as transforms, because resizing the bar by layout would re-render its glass and re-measure the native control every frame. Panes are never swapped: `components/attachment/PaneLayer.tsx` stacks every visited pane on `StyleSheet.absoluteFill` and cross-fades opacity with `withTiming`, which is what preserves scroll position and typed text. Two iOS quirks are handled explicitly in Attachment Sheet: a `UIVisualEffectView` installs its effect on a single first layout pass, and that pass happens while the sheet is still invisible, so every glass layer is remounted through a `glassKey` once the open spring reports finished; and `UISegmentedControl` eats a horizontal drag before any React Native `ScrollView` can see it, so `modules/segmented-control/ios/SegmentedControlView.swift` puts the control inside its own `UIScrollView` and rasterizes each icon plus label pair into a single `UIImage`, since a segment can carry a title or an image but not both. ## What you get ``` App.tsx index.ts app.json package.json yarn.lock tsconfig.json babel.config.js README.md components/ Container.tsx attachment/ AttachmentSheet.tsx GalleryPane.tsx Glass.tsx MenuBar.tsx PaneLayer.tsx SheetHeader.tsx photos.ts segments.ts theme.ts panes/ AudioPane.tsx ChecklistPane.tsx ContactPane.tsx FilePane.tsx GalleryNotice.tsx LocationPane.tsx PollPane.tsx TextPane.tsx chat/ ChatBackground.tsx ChatHeader.tsx GroupInfoBubble.tsx InputBar.tsx modules/ segmented-control/ index.ts expo-module.config.json ios/ SegmentedControlModule.swift SegmentedControlView.swift SegmentedControl.podspec assets/ ``` Included in the Attachment Sheet download from Motionary: - `AttachmentSheet.tsx`, the full morph plus drag-to-detent sheet - Eight ready-made panes (gallery, text, file, location, poll, checklist, audio, contact) - A custom Expo native module: a scrolling `UISegmentedControl` in Swift - A recycled three column photo grid built on Legend List and `expo-image` - `theme.ts`, every spring, inset and radius measured off the reference recording - A chat mock screen (header, wallpaper, input bar) to demo it in context - README with install and usage - MP4 preview ## Requirements Attachment Sheet from Motionary is pinned in its `package.json` to: - `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 - `react-native-gesture-handler` ~2.31.1 - `react-native-safe-area-context` ~5.7.0 - `expo-glass-effect` ~56.0.4 - `expo-symbols` ~56.0.6 - `expo-haptics` ~56.0.3 - `expo-image` ~56.0.11 - `expo-linear-gradient` ~56.0.4 - `expo-status-bar` ~56.0.4 - `@legendapp/list` 3.3.3 - `typescript` ~6.0.3 (dev dependency) The React Compiler is enabled through `experiments.reactCompiler` in `app.json`, and `expo.autolinking.nativeModulesDir` in `package.json` points at `./modules` so the bundled Swift module is picked up. Attachment Sheet as shipped needs a development build, but not for the reason usually given. Every Expo module it imports is bundled in Expo Go on Expo SDK 56, `expo-glass-effect` included, so the liquid glass does not force a build, whatever older React Native writing says; neither do `react-native-reanimated`, `react-native-gesture-handler`, `expo-symbols`, `expo-haptics`, `expo-image` or Legend List. What forces one here is the project's own Swift Expo module in `modules/segmented-control`, wrapping UIKit's `UISegmentedControl`: a local native module is not in Expo Go, so run Attachment Sheet with `npx expo run:ios`. Swap that one bar for a JavaScript segmented control and everything else in Attachment Sheet runs in Expo Go. A development build is what you would ship to production in any case, and it is what you need as soon as you add a native module Expo Go does not bundle, or a config plugin. Two caveats outlast the build question. The native segmented control is iOS only (`isSegmentedControlAvailable` is `Platform.OS === 'ios'` and the bar renders nothing elsewhere), so Attachment Sheet is an iOS first drop. And liquid glass draws its real material only where iOS offers it: everywhere else `isLiquidGlassAvailable()` is false and the bar and buttons wash to near white. ## Install ```bash npx expo install react-native-reanimated react-native-worklets \ react-native-gesture-handler react-native-safe-area-context \ expo-glass-effect expo-symbols expo-haptics expo-image \ expo-linear-gradient expo-status-bar @legendapp/list npx expo run:ios ``` ## Usage ```tsx import { StyleSheet, View } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { StatusBar } from 'expo-status-bar'; import { AttachmentSheet } from './components/attachment/AttachmentSheet'; import { InputBar } from './components/chat/InputBar'; export default function App() { return ( {/* your chat messages go here */} {/* AttachmentSheet takes no props: it renders the paperclip button itself, overlaid on the input bar, and morphs it into the sheet. */} ); } ``` `AttachmentSheet` must sit last inside a `GestureHandlerRootView` and a `SafeAreaProvider`: it renders as an absolutely filled overlay with `pointerEvents="box-none"`, and it reads `useSafeAreaInsets()` to place the paperclip button over the slot `InputBar` reserves for it. ## Customization `AttachmentSheet` itself takes no props. Everything is tuned in `components/attachment/theme.ts` and `components/attachment/segments.ts`: - `springs.open`: `{ duration: 320, dampingRatio: 0.83 }`, the open morph - `springs.close`: `{ duration: 220, dampingRatio: 1 }`, quicker, no bounce - `springs.detent`: `{ duration: 320, dampingRatio: 0.9 }`, the drag snap - `backdropTiming`: `{ open: { duration: 420 }, close: { duration: 260 } }` - `colors.backdrop`: `rgba(0, 0, 0, 0.28)`, the flat wallpaper dim - `sheet.heightRatio`: `578 / 874`, the resting detent as a share of the screen - `sheet.radius`: `38`, `sheet.expandedBottomRadius`: `54` - `sheet.sideInset` / `sheet.bottomInset`: `6`, `sheet.expandedTopInset`: `10` - `sheet.dismissTravel`: `110`, downward drag before the sheet dismisses - `sheet.overshootResistance`: `0.35`, drag past the expanded detent - `paneTransition`: `fadeDuration` 150, `dip` 10, `dipDuration` 180, `recover: { duration: 340, dampingRatio: 0.66 }` - `menuBar`: `height` 58, `radius` 28, `sideInset` 15.5, `iconSize` 25, `fontSize` 12.5, `iconSpacing` 8, `verticalInset` 2 - `grid`: `columns` 3, `gap` two physical pixels - `colors.accent` `#3A88B0`, `colors.sheet` `#F0F0F0` - `SEGMENTS` in `segments.ts`: each entry is `{ key, title, systemImage, headerTitle, withDisclosure?, trailing? }` where `systemImage` is an SF Symbol name and `trailing` is `'search' | 'undo-redo'`. Add, remove or reorder segments here and the native bar re-lays itself out. The bundled `SegmentedControl` view accepts `segments`, `selectedIndex`, `onSelect`, `selectedContentColor`, `contentColor`, `indicatorColor`, `iconSize` (default 24), `fontSize` (default 11), `iconSpacing` (default 3), `iconWeight` (default `'medium'`), `fontWeight` (default `'regular'`), `contentPadding` and `verticalInset`. It is `onSelect`, not `onChange`, because React Native reserves `onChange` as a bubbling event. ## When to use it - A chat or messaging screen that needs a real attachment picker behind the paperclip, instead of a plain modal. - A composer where the attach button should feel like it opened the sheet, not like it triggered something separate. - A multi mode picker (photos, files, location, polls) where switching modes must preserve half typed input and scroll position. - An iOS app that wants a scrolling segmented control with more items than fit, with UIKit's own selection indicator rather than a hand drawn one. - A liquid glass surface on Expo SDK 56 that needs a graceful white fallback. - Any bottom sheet with two detents, drag to expand and flick to dismiss, where you would rather own the source than configure a library. ## How to get it - Buy Attachment Sheet on its own at https://motionary.dev/animations/attachment-sheet, $12 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 Telegram style attachment sheet in React Native?** Keep the paperclip button and the sheet as one view and interpolate its frame, rather than animating a separate modal in: Motionary's Attachment Sheet drives `left`, `top`, `width`, `height` and four corner radii from a single Reanimated shared value, lays the contents out once at full size and scales them, and runs the backdrop dim on its own timing curve. **Does Attachment Sheet work with Expo Go?** Not as shipped, and `expo-glass-effect` is not the reason: it is bundled in Expo Go on Expo SDK 56, as is every other Expo module Motionary's Attachment Sheet imports. What needs a development build is the drop's own Swift Expo module wrapping UIKit's `UISegmentedControl`, since a local native module is not in Expo Go, so run it with `npx expo run:ios`. A development build is what you would ship to production anyway. **Does Attachment Sheet work on Android?** The morph, the pan gesture, the detents and the eight panes are plain React Native and Reanimated, but Attachment Sheet's segment bar is a native iOS `UISegmentedControl` (`isSegmentedControlAvailable` is `Platform.OS === 'ios'`), so it is an iOS first drop and Android needs its own bar implementation. **How do I make a React Native bottom sheet snap to detents and flick away?** Attachment Sheet writes a `sheetTop` shared value from a `Gesture.Pan`, applies an overshoot resistance factor past the expanded detent, and on release picks a detent from position and velocity, dismissing outright above 900 downward velocity or past 110pt of travel. Because the bottom edge is pinned, dragging up grows the sheet and dragging down slides the whole thing away. **Can a segmented control scroll horizontally in React Native?** Not inside a React Native `ScrollView`, because `UISegmentedControl` consumes the drag first. Motionary's Attachment Sheet solves it in `modules/segmented-control/ios/SegmentedControlView.swift` by putting the control inside a `UIScrollView` on the native side and sizing every segment from its content padding. **Why does my expo-glass-effect view render blank inside an animated sheet?** A `UIVisualEffectView` installs its effect on a single first layout pass and renders nothing while it or an ancestor sits at zero opacity, so a glass layer that first laid out during the morph stays empty. Attachment Sheet remounts every glass layer through a `glassKey` once the open spring reports finished. **How much does Attachment Sheet cost and what do I get?** Attachment Sheet is $12 USD on Motionary at https://motionary.dev/animations/attachment-sheet (price as of 2026-09-01), or free with the $79 USD Motionary lifetime pass, and you get the full editable TypeScript source plus the Swift native module as an instant download. ## Related drops on Motionary - ChatGPT-Style Attachments Menu ($10): https://motionary.dev/animations/chatgpt-style-attachments-menu The same idea from a different app: an attachments button that morphs into a liquid glass menu over a chat composer. - Mini-Player Sheet (Free): https://motionary.dev/animations/mini-player-sheet A free bottom sheet whose whole expansion is one interpolated shared value, a good way to read the technique before buying Attachment Sheet. - Island Genie ($11): https://motionary.dev/animations/island-genie Another bottom sheet that grows out of a small element, here the Dynamic Island, with a Skia genie warp instead of a frame interpolation. - Telegram Swipe Actions ($10): https://motionary.dev/animations/telegram-swipe-actions The Telegram chat list to Attachment Sheet's Telegram chat screen: swipeable rows built on Reanimated and Gesture Handler. - Long-Press Menu ($9): https://motionary.dev/animations/long-press-menu A glass menu that opens off a chat input bar, for the other half of a messaging composer. ## 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/attachment-sheet/llms.txt Describes: https://motionary.dev/animations/attachment-sheet Format: llms.txt (https://llmstxt.org) Last updated: 2026-09-01 ```