[ Documentation ]
peach-kit

peach-kit

peach-kit is what the simulator gives your stub files beyond react and react-native: the iOS feedback generators, iOS spring physics, a progressive blur, SwiftUI’s numeric text transition, SwiftUI views, and access to the simulated device. Use it when you write a module or view file for peach.config.ts, so your replacement behaves the way the native code it stands in for does.

You do not install peach-kit. Peach supplies each entry point at runtime to the files your peach.config.ts names, and your app never bundles it. A stub file may import peach-kit and any of the subpaths below.

importwhat it gives you
peach-kit/hapticsthe iOS impact, notification and selection feedback generators
peach-kit/animationSwiftUI, UIKit and Core Animation springs as Animated.spring parameters
peach-kit/materialsProgressiveBlurView, a variable blur with a gradient mask
peach-kit/textNumericText, SwiftUI’s numeric content transition
peach-kit/uiSwiftUI views, the same set Peach renders for @expo/ui/swift-ui
peach-kit/runtimethe platform, permissions, notifications and alerts of the simulated device, and React bound to your app
peach-kitpeach-kit/runtime and peach-kit/ui together

haptics, animation and runtime are always loaded. materials, text and ui load on demand the first time your app or a stub file needs them. The package’s two other entry points, peach-kit/native-state and peach-kit/internal, belong to the engine and are not available to stub files.

peach-kit/haptics

The iOS feedback generators. Every haptics library Peach supports plays through this same implementation. A browser has no Taptic Engine, so each call plays a vibration where the browser can vibrate and always shows the tap on the device frame. Like the real generators, these never throw.

import { haptics } from 'peach-kit/haptics'
haptics.impact('medium')
haptics.notification('success')
haptics.selection()
exportwhat it does
haptics.impact(style)UIImpactFeedbackGenerator(style:).impactOccurred(). style is 'light', 'medium', 'heavy', 'soft' or 'rigid'.
haptics.notification(type)UINotificationFeedbackGenerator().notificationOccurred(_:). type is 'success', 'warning' or 'error'.
haptics.selection()UISelectionFeedbackGenerator().selectionChanged()
playHapticPattern(pattern)plays a raw pattern: one pulse in milliseconds, or alternating pulse and pause milliseconds. Use it for Android haptic constants and pattern-based libraries.
hapticStrength(pattern)the 0 to 1 strength the device frame draws for a pattern
isImpactStyle(value), isNotificationType(value)narrow a string your native prop or method argument carried

Types: ImpactStyle, NotificationType, HapticPattern.

peach-kit/animation

iOS springs as the physical spring Animated.spring takes. Peach’s Animated.spring solves the spring in closed form, the way CASpringAnimation does, so a spring converted here moves exactly as the iOS spring does.

import { Animated } from 'react-native'
import { spring } from 'peach-kit/animation'
Animated.spring(offset, {
...spring({ duration: 0.4, bounce: 0.2 }),
toValue: 0,
useNativeDriver: true,
}).start()

spring(parameters) returns { mass, stiffness, damping } from any of three forms:

parametersiOS API
{ response, dampingFraction }SwiftUI Spring(response:dampingFraction:). A dampingFraction of 1 is critically damped.
{ duration, bounce? }SwiftUI Spring(duration:bounce:) and UIKit UIView.animate(springDuration:bounce:). A bounce of 0 is critically damped, up to 1 is bouncier, below 0 is overdamped.
{ mass?, stiffness, damping }CASpringAnimation. mass defaults to 1.

springResponse({ duration, bounce }) converts a duration and bounce into the equivalent { response, dampingFraction }.

UIKit’s older UIView.animate(withDuration:usingSpringWithDamping:) fits its spring to a total duration through an undocumented mapping, so it has no form here.

Types: SpringParameters, SpringPhysics.

peach-kit/materials

ProgressiveBlurView reproduces a UIKit UIVisualEffectView whose backdrop uses a variable blur with a gradient mask: the blur radius changes along the mask, and children draw above the blurred backdrop.

// peach/HeaderBlur.tsx
import { ProgressiveBlurView } from 'peach-kit/materials'
import type { ProgressiveBlurViewProps } from 'peach-kit/materials'
export default function HeaderBlur(props: ProgressiveBlurViewProps) {
return <ProgressiveBlurView {...props} />
}

Point your app’s native view at that file:

// peach.config.ts
import { defineConfig } from 'peachsim/config'
export default defineConfig({
packages: {
'my-app': { views: { AppHeaderBlur: './peach/HeaderBlur.tsx' } },
},
})

Your app’s requireNativeComponent('AppHeaderBlur') now renders the replacement with the props your app passes. If your native view names its props differently, translate them to these:

proptypemeaning
maxRadiusnumbermaximum blur radius, in points
direction'top' | 'bottom' | 'center'where the mask reaches its maximum radius
startOffsetnumberthe opaque plateau at the blurred edge, as a fraction. For center, the clear inset, from 0.01 to 0.3. Defaults to 0.
blurStyleBlurStylethe UIBlurEffect.Style. Defaults to 'regular'.
styleview stylelayout, clipping and other view styling

ProgressiveBlurView accepts every other View prop and a ref to the view.

BlurStyle covers the five system materials (systemUltraThinMaterial, systemThinMaterial, systemMaterial, systemThickMaterial, systemChromeMaterial), their Light and Dark variants, and extraLight, light, dark, regular and prominent. The variable blur replaces the material’s overlays, so it adds no material tint. The mask follows the cubic gradient of @sbaiahmed1/react-native-blur 6.0.1, and the center direction uses two fixed 20% fade bands around an opaque center. The view is checked against native captures on iPhone 17 Pro.

Types: ProgressiveBlurViewProps, BlurStyle.

peach-kit/text

NumericText shows a formatted number and plays SwiftUI’s numeric content transition when its value changes inside an animation. Use it when your native view displays a number with .contentTransition(.numericText()).

// peach/BalanceLabel.tsx
import { NumericText } from 'peach-kit/text'
import type { NumericTextProps } from 'peach-kit/text'
export default function BalanceLabel(props: NumericTextProps) {
return <NumericText {...props} />
}
<NumericText value={balance} format={{ style: 'currency', currency: 'USD' }} animation={{ kind: 'default' }} style={{ fontSize: 24 }} />
proptypemeaning
valuenumberthe value ContentTransition.numericText(value:) animates
formatIntl.NumberFormatOptionshow the number is displayed, including currency
countsDownbooleanselects SwiftUI’s numericText(countsDown:). Omit it to use the value: form.
animationSwiftAnimation | nullthe withAnimation transaction, such as { kind: 'default' }. kind is 'default', 'timing', 'spring', 'interpolatingSpring' or 'repeat', with the matching SwiftUI parameters. Omit it to inherit the surrounding transaction; null turns the transition off.

NumericText accepts every other Text prop and a ref to the text. A string child replaces the formatted value with text your native code already formatted.

Types: NumericTextProps, SwiftAnimation.

peach-kit/ui

The SwiftUI views Peach renders when an app uses @expo/ui/swift-ui, available to your stub files directly. Use them when the native view you are replacing is built in SwiftUI, so your replacement draws the same controls. Put them inside a Host, as with @expo/ui.

// peach/StatusRow.tsx
import { Host, HStack, Text, Toggle } from 'peach-kit/ui'
export default function StatusRow({ title, on, onChange }: {
title: string
on: boolean
onChange: (on: boolean) => void
}) {
return (
<Host style={{ height: 44 }}>
<HStack spacing={8}>
<Text>{title}</Text>
<Toggle isOn={on} onIsOnChange={onChange} />
</HStack>
</Host>
)
}

The views follow the @expo/ui/swift-ui API:

  • +
    Hosting and layout: Host, HStack, VStack, ZStack, LazyHStack, LazyVStack, Grid, LazyHGrid, LazyVGrid, Group, Spacer, Divider, GeometryReader, ScrollView, GlassEffectContainer, Namespace
  • +
    Lists and forms: List, ListForEach, Form, Section, GroupBox, DisclosureGroup, LabeledContent, Label, SwipeActions, ContentUnavailableView
  • +
    Controls: Button, Toggle, Switch, Slider, Stepper, Picker, SegmentedControl, DatePicker, MultiDatePicker, ColorPicker, Checkbox, TextField, SecureField, Gauge, ProgressView, ControlGroup, PageControl, TabView, Link, ShareLink
  • +
    Menus and presentation: Menu, ContextMenu (with Trigger, Items and Preview), Popover, BottomSheet, FullScreenCover, Alert, ConfirmationDialog
  • +
    Content and shapes: Text, Image, Chart, Map, LinearGradient, Rectangle, RoundedRectangle, UnevenRoundedRectangle, ConcentricRectangle, Capsule, Circle, Ellipse, Path, Overlay, Background, Mask, RNHostView

peach-kit/ui also exports the modifier names @expo/ui has, such as padding, frame and foregroundStyle, so imports written for @expo/ui resolve. In a stub file they are placeholders that the views ignore: lay out and style with the views’ own props and style. Its other exports are helpers Peach’s own adapters share, and are not a stable API.

peach-kit/runtime

Read access to the simulated device, for stub files whose native side would ask the operating system something.

exportwhat it returns
getPlatform()'ios' or 'android', the platform of the simulated device
getSystemPermissions()the device’s permissions: read a permission’s status, check one, or request it
getNativeNotifications()the device’s notification center, for presenting and dismissing notifications
getNativeAlerts()the platform alert presenter, with Alert.alert’s arguments
getReactNative()the react-native module your app runs on
getEngine(), getEngineConfig(), getOptionalEngineConfig()the simulator’s full service object, which Peach’s own stubs build on

peach-kit/runtime also re-exports React (useState, useEffect, createElement, Fragment, forwardRef, memo and the rest of the React 19 API, plus the default export) bound to the React instance your app runs on. A stub file’s react import already resolves to that same instance, so you only need these when writing a module that cannot import react itself.