Core Every number the sheet computes — snap points, keyboard, footer, geometry, the step machine — as pure functions with no native module in the graph.
Copy MarkdownScan to preview Open in app import { dynamicSnapPoint, keyboardLift, positionFor, UNMEASURED } from "@delacour/react-native-bottom-sheet/core" ;
@delacour/react-native-bottom-sheet/core is every number the package computes and nothing that
renders. Nothing under it imports a runtime value from React, React Native, Reanimated, Gesture
Handler, keyboard-controller, safe-area-context or teleport, which is what lets a skin re-export the
footer's inset rules without mounting a sheet — and what lets the whole of it, the footer-on-keyboard
proof included, run under bun test with no simulator.
import { footerTop, keyboardLift } from "@delacour/react-native-bottom-sheet/core" ;
const lift = keyboardLift ({ keyboardHeight: 336 , progress: 1 , band: 34 , behavior: "interactive" , gapBelow: 0 });
const top = footerTop ({ sheetHeight: 420 + lift, keyboardHeight: 336 , footerContentHeight: 72 , band: 0 });
Every value is a height : pixels of sheet visible above its resting bottom line. 0 is closed,
snap points ascend, and UNMEASURED (-1) is a measurement that has not happened. translateY is
derived from a height only at the moment a view needs it, by positionFor, and nowhere else.
With C the container height, inset the safe-area bottom, kb the owned keyboard within the
container and p keyboard-controller's progress:
restingBottom = detached ? bottomOffset + inset : 0 closedHeight = −restingBottom
available = C − restingBottom maxHeight = available
band = detached ? 0 : inset bandNow = band · (1 − p)
footerHeight = hasFooter ? footerContent + bandNow : 0
dynamicSnapPoint = min(handle + content + (hasFooter ? footerContent : 0) + band, maxDynamicContentSize, available)
snapPoints = sortAsc(unique(normalize(spec, available) ∪ {dynamicSnapPoint if dynamicSizing}))
keyboardLift = behavior ∈ {interactive, extend} ? max(0, kb − band · p − (detached ? restingBottom : 0)) : 0
height = clamp(base + keyboardLift, closedHeight, maxHeight)
translateY = C − restingBottom − height
index = piecewise(base, [closedHeight, …snapPoints] → [−1, 0, 1, …]) base, never height
contentArea = max(0, min(maxHeight, highest + keyboardLift) − handle − footerHeight − kb − (hasFooter ? 0 : bandNow))
footerTop = max(0, height − kb − footerContent − bandNow)
backdrop = index ≥ appearsOn ? opacity : index ≤ disappearsOn ? 0 : linear
base is the one value a gesture or an animation writes — the keyboard-free height. Everything
else is derived from it on the UI thread, one useDerivedValue per line above.
Most functions here carry a "worklet" directive and run on the UI thread every frame of a drag.
Each calls nothing and closes over nothing but module-scope constants — a module-scope worklet that
called another would bind at import time and hand the UI thread undefined is not a function the
moment a finger landed. A worklet you build inside a hook is captured by ordinary closure and
may call any of them, which is how the pan composes resistOverDrag and selectSnapHeight without
duplicating either.
A degenerate input returns a finite number, never NaN: one NaN written into base freezes a
sheet permanently with nothing on screen to say why.
Export What it does parseSnapPoint(spec)One SnapPointSpec to { kind: "px" | "percent", value }, as a Result normalizeSnapPoints(spec, available)Ascending, unique, clamped; invalid entries skipped. Worklet dynamicSnapPoint({ handleHeight, contentHeight, footerContentHeight, hasFooter, band, available, maxDynamicContentSize? })The content's own snap point. Worklet indexForHeight(height, snapPoints, closedHeight), heightForIndex(index, snapPoints, closedHeight)Piecewise-linear over [closed, …snapPoints], clamped. Worklets selectSnapHeight({ height, velocity, snapPoints, closedHeight, projection })The candidate nearest height + projection · velocity. Worklet resistOverDrag(height, min, max, factor)Slope one at the edge, square root beyond; factor 0 clamps. Worklet sheetState(base, height, snapPoints, closedHeight, maxHeight)SHEET_STATE.CLOSED | OPENED | EXTENDED | OVER_EXTENDED | FILL. Worklet
Export What it does keyboardInContainer(rawKeyboardHeight, containerBottomOffset)keyboard-controller's negative height to the positive overlap with the frame. Worklet keyboardLift({ keyboardHeight, progress, band, behavior, gapBelow })The lift: keyboard less the band in step with progress, less a detached gap. Worklet resolveKeyboardOwner(input)Whose keyboard it is, under keyboardScope, sticky through a transition isInputInsideSheet(input)The inside scope's geometry test, in window space. Worklet keyboardStep(input)What a behaviour does this frame: apply, restore, release or nothing contentArea({ sheetHeight, handleHeight, footerHeight, keyboardHeight, trailingBand })What the body may fill, never negative. Worklet acceptContainerLayout(input)Refuses Android's adjustResize double-count while a keyboard is up shouldResetKeyboardAnimation(state)The stale keyboard-controller guard. Worklet
Export What it does bottomBand(detached, bottomInset), bandNow(band, progress), footerHeight(hasFooter, footerContentHeight, band)The band and what the footer occupies. Worklets footerTop({ sheetHeight, keyboardHeight, footerContentHeight, band })The footer's offset inside the panel — constant across a keyboard animation. Worklet resolveSheetBottomInset, resolveSheetScrollEndPaddingThe skin's padding rules, re-exported by it
Export What it does restingBottom(detached, bottomOffset, bottomInset), closedHeight(restingBottom), availableHeight(containerHeight, restingBottom)Where the sheet rests, what closed is, what it may fill. Worklets positionFor(containerHeight, restingBottom, height)Height to translateY, the only conversion. Worklet clampHeight(height, closedHeight, maxHeight)NaN collapses to closed. WorkletsurfaceHeight(height, lowestSnapPoint, highestSnapPoint)What a detached card's surface is tall — between its snap points, the nearest snap point beyond them. Worklet resolveDetached(prop), DETACHED_DEFAULTSThe detached prop to { horizontalMargin, bottomOffset } or null detachedFrame({ containerWidth, horizontalMargin, bottomOffset, bottomInset }){ left, width, restingBottom }. Worklet
Export What it does backdropOpacity(index, disappearsOn, appearsOn, opacity), backdropInteractive(index, disappearsOn)What the overlay draws at, and whether it takes touches. Worklets resolveIntent(state, intent){ action: "wait" | "animate" | "jump" } or null — what a ref method does given where the sheet is. WorkletisLayoutReady(input)Whether every measurement the configuration needs has landed. Worklet crossedSnapPoint(lastIndex, height, snapPoints), snapPointUnder(height, snapPoints)Whether a frame's travel crossed a snap point, and which is under a height. Worklets shouldLockScroll(state, enableContentPan), contentPanDrivesSheet({ locked, contentOffsetY, translationY })The scroll lock's two questions. Worklets listDragHeight(input), listOwnsRelease({ scrollable, offset, base, highest }), scrollLockTarget(offset, byContentPan)The start offset as a budget the finger spends, whose the release is, where the lock holds. Worklets
Export What it does selectAnimation(config, platform, overrideReduceMotion)The consumer's config over the platform default, every field filled, as data IOS_SPRING, ANDROID_TIMINGThe two defaults SheetAnimationConfig, ResolvedAnimation, EasingName, ReduceMotionModeThe types
Export What it does defineSheetMachine(config){ config, initial, transition, can, steps, directionOf, nodeOf }transition(states, snapshot, event)The one step, as a Result<Snapshot, SheetTransitionError> — never throws, never mutates resolveTarget(target)A bare target or an object to { target, guard?, assign? } stepFrame(transition, role, direction, progress, width)A step's opacity and offset mid-change, for crossfade, slide and none. Worklet stepOverride(node)The snapPoints and dismissible a step asks of the root SheetMachine, SheetMachineConfig, SheetStateNode, SheetMachineSnapshot, SheetTransitionErrorThe types
Export What it is SnapPointSpecnumber | `${number}%`KeyboardBehavior, KeyboardBlurBehavior, KeyboardScopeThe three keyboard unions AnimationSource"mount" | "gesture" | "user" | "snapPoints" | "keyboard" | "container"SheetIntentThe request a ref method writes, keyed by id so two identical requests both run GESTURE_SOURCE, SHEET_STATE, SCROLLABLE_TYPENumeric enums — a shared value holding a string serialises on every write UNMEASURED, CLOSED_INDEX-1 and -1Result, ok, errThe house result union, copied in so the package depends on nothing private