@rific/scroll-view 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jay Deaton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -14,7 +14,7 @@ npm install @rific/scroll-view
14
14
  npm install @rific/auto-paper react-native-reanimated react-native-gesture-handler react-native-safe-area-context react-native-paper react-native-keyboard-controller
15
15
  ```
16
16
 
17
- The exported Redux slice has no dependency on `@reduxjs/toolkit` — it works with RTK stores, vanilla Redux, or no Redux at all.
17
+ The exported Redux slice has no dependency on `@reduxjs/toolkit` - it works with RTK stores, vanilla Redux, or no Redux at all.
18
18
 
19
19
  ## Quick start
20
20
 
@@ -57,7 +57,7 @@ App-wide defaults for scroll settings. Wrap your root layout to set global defau
57
57
 
58
58
  | Prop | Type | Description |
59
59
  |------|------|-------------|
60
- | `children` | `ReactNode` | — |
60
+ | `children` | `ReactNode` | - |
61
61
  | `initialValue` | `Partial<ScrollViewSettings>` | Initial settings applied once on mount |
62
62
  | `onChange` | `(settings: ScrollViewSettings) => void` | Called whenever settings change |
63
63
 
@@ -70,15 +70,15 @@ Screen-level provider that owns header/footer state and scroll position. Must wr
70
70
  | Prop | Type | Default | Description |
71
71
  |------|------|---------|-------------|
72
72
  | `blur` | `boolean` | system | Enable frosted-glass backdrop on header/footer |
73
- | `chrome` | `boolean` | `true` | Join an enclosing [`ScrollViewChromeProvider`](#scrollviewchromeprovider) when there is one. Pass `false` to opt out — for a nested provider that isn't a screen, such as a picker or form modal. No effect when nothing hosts |
73
+ | `chrome` | `boolean` | `true` | Join an enclosing [`ScrollViewChromeProvider`](#scrollviewchromeprovider) when there is one. Pass `false` to opt out - for a nested provider that isn't a screen, such as a picker or form modal. No effect when nothing hosts |
74
74
  | `fixed` | `boolean` | `false` | Pin both header and footer (overrides `headerFixed`/`footerFixed`) |
75
- | `footerAboveKeyboard` | `boolean` | `false` | With a fixed footer, float it above the keyboard instead of letting the keyboard cover it — see [Keyboard awareness](#keyboard-awareness) |
75
+ | `footerAboveKeyboard` | `boolean` | `false` | With a fixed footer, float it above the keyboard instead of letting the keyboard cover it - see [Keyboard awareness](#keyboard-awareness) |
76
76
  | `headerFixed` | `boolean` | `false` | Pin header; overrides settings default |
77
77
  | `footerFixed` | `boolean` | `false` | Pin footer; overrides settings default |
78
78
  | `snapBack` | `boolean` | `false` | Snap header and footer back when scrolling up |
79
- | `snapBackHeader` | `boolean` | — | Override `snapBack` for header only |
80
- | `snapBackFooter` | `boolean` | — | Override `snapBack` for footer only |
81
- | `tabBarHeight` | `number` | `0` | Extra bottom inset reserved for a host app's own persistent tab bar (added on top of the safe-area inset, independent of any `ScrollViewFooter`). Ignored inside a `ScrollViewChromeProvider` — the host's `footprint` replaces it (see [Persistent bottom chrome](#persistent-bottom-chrome-hiding-a-tab-bar-with-scroll)) |
79
+ | `snapBackHeader` | `boolean` | - | Override `snapBack` for header only |
80
+ | `snapBackFooter` | `boolean` | - | Override `snapBack` for footer only |
81
+ | `tabBarHeight` | `number` | `0` | Extra bottom inset reserved for a host app's own persistent tab bar (added on top of the safe-area inset, independent of any `ScrollViewFooter`). Ignored inside a `ScrollViewChromeProvider` - the host's `footprint` replaces it (see [Persistent bottom chrome](#persistent-bottom-chrome-hiding-a-tab-bar-with-scroll)) |
82
82
 
83
83
  ---
84
84
 
@@ -88,13 +88,13 @@ Hosts a persistent bottom bar (a tab bar) so it hides on scroll together with ea
88
88
 
89
89
  | Prop | Type | Default | Description |
90
90
  |------|------|---------|-------------|
91
- | `children` | `ReactNode` | — | Usually the navigator (`<Tabs>`) |
92
- | `footprint` | `number` | required | Height of the bar's resting box, including its bottom safe-area inset — how far above the screen bottom a hosted `ScrollViewFooter` has to sit to clear it. A plain number rather than something the bar reports back, so every screen has the right geometry on its very first render |
91
+ | `children` | `ReactNode` | - | Usually the navigator (`<Tabs>`) |
92
+ | `footprint` | `number` | required | Height of the bar's resting box, including its bottom safe-area inset - how far above the screen bottom a hosted `ScrollViewFooter` has to sit to clear it. A plain number rather than something the bar reports back, so every screen has the right geometry on its very first render |
93
93
  | `overhang` | `number` | `0` | How far the bar pokes *above* its resting box (a raised centre button, say). Hosted footers pad their bottom by this instead of the safe-area inset, and the hide distance covers it |
94
94
  | `snapBack` | `boolean` | `true` | Hosted footers reveal on a short scroll-up instead of only near the top. A bar that hides on scroll has to be reachable from anywhere in a list, so leave this on unless you have a reason not to |
95
- | `useIsActive` | `() => boolean` | `() => true` | A hook returning whether the calling screen is the focused one — pass react-navigation's `useIsFocused` (re-exported by `expo-router`). Only the focused screen drives the bar. Injected so this package carries no navigation dependency. **Must be a stable hook reference** (a module-level hook such as `useIsFocused`, never an inline arrow created per render): it is called as a hook on every render of every hosted provider |
95
+ | `useIsActive` | `() => boolean` | `() => true` | A hook returning whether the calling screen is the focused one - pass react-navigation's `useIsFocused` (re-exported by `expo-router`). Only the focused screen drives the bar. Injected so this package carries no navigation dependency. **Must be a stable hook reference** (a module-level hook such as `useIsFocused`, never an inline arrow created per render): it is called as a hook on every render of every hosted provider |
96
96
 
97
- The default `() => true` makes every mounted hosted screen claim the host, and the last one to mount owns it — only right for a navigator that keeps a single screen mounted. Use `useIsFocused` with `Tabs` or anything else that keeps visited screens alive.
97
+ The default `() => true` makes every mounted hosted screen claim the host, and the last one to mount owns it - only right for a navigator that keeps a single screen mounted. Use `useIsFocused` with `Tabs` or anything else that keeps visited screens alive.
98
98
 
99
99
  ---
100
100
 
@@ -104,18 +104,18 @@ Floating header with blur backdrop, title, back action, trailing action, and bui
104
104
 
105
105
  | Prop | Type | Default | Description |
106
106
  |------|------|---------|-------------|
107
- | `title` | `string` | — | Center title text |
108
- | `caption` | `string` | — | Subtitle below title |
109
- | `centerContent` | `ReactNode` | — | Replaces title/caption with custom content |
110
- | `children` | `ReactNode` | — | Left-side content (icon buttons, etc.) |
111
- | `backAction` | `() => void` | — | Renders a back button and calls this on press |
107
+ | `title` | `string` | - | Center title text |
108
+ | `caption` | `string` | - | Subtitle below title |
109
+ | `centerContent` | `ReactNode` | - | Replaces title/caption with custom content |
110
+ | `children` | `ReactNode` | - | Left-side content (icon buttons, etc.) |
111
+ | `backAction` | `() => void` | - | Renders a back button and calls this on press |
112
112
  | `backActionFixed` | `boolean` | settings | Keep back button visible as header scrolls away |
113
- | `trailingAction` | `ReactNode` | — | Right-side floating action |
113
+ | `trailingAction` | `ReactNode` | - | Right-side floating action |
114
114
  | `trailingActionFixed` | `boolean` | `true` | Keep trailing action visible as header scrolls away |
115
115
  | `actionSize` | `number` | `48` | Size of floating action buttons |
116
116
  | `iconSize` | `number` | `actionSize/2` | Icon size inside action buttons |
117
- | `actionStyle` | `ViewStyle` | — | Style applied to action button backgrounds |
118
- | `style` | `ViewStyle` | — | Style applied to the header content area |
117
+ | `actionStyle` | `ViewStyle` | - | Style applied to action button backgrounds |
118
+ | `style` | `ViewStyle` | - | Style applied to the header content area |
119
119
  | `topInset` | `boolean` | `true` | Pad for safe area top inset |
120
120
 
121
121
  ---
@@ -189,7 +189,7 @@ When `horizontal` and `pagingEnabled` are both true and `data` has more than one
189
189
 
190
190
  ### `SectionList`
191
191
 
192
- Drop-in for React Native's `SectionList`. Accepts all `SectionListProps<ItemT, SectionT>` (except `horizontal`) plus the same set of library props as `FlatList`. Custom sticky section headers are supported — pass `renderSectionHeader` and `stickySectionHeadersEnabled` and the library renders them via an animated overlay so they properly account for the floating header offset.
192
+ Drop-in for React Native's `SectionList`. Accepts all `SectionListProps<ItemT, SectionT>` (except `horizontal`) plus the same set of library props as `FlatList`. Custom sticky section headers are supported - pass `renderSectionHeader` and `stickySectionHeadersEnabled` and the library renders them via an animated overlay so they properly account for the floating header offset.
193
193
 
194
194
  ---
195
195
 
@@ -218,7 +218,7 @@ import { CustomList } from '@rific/scroll-view'
218
218
  | `renderFilters` | `ReactNode` | Rendered below `ListHeaderComponent`, above list items |
219
219
  | `scrollRef` | `RefObject<{ scrollToOffset }>` | Forward ref to the underlying list |
220
220
 
221
- The underlying component receives `contentInset`, `contentOffset`, `onScroll`, `refreshControl`, and `scrollEventThrottle` — props it must support for the library to function.
221
+ The underlying component receives `contentInset`, `contentOffset`, `onScroll`, `refreshControl`, and `scrollEventThrottle` - props it must support for the library to function.
222
222
 
223
223
  ---
224
224
 
@@ -282,7 +282,7 @@ export default function SearchScreen() {
282
282
  | Prop | Type | Description |
283
283
  |------|------|-------------|
284
284
  | `onChangeText` | `(text: string) => void` | Called with trimmed search text |
285
- | `onHeightChange` | `(height: number) => void` | Required — pass the height back as `pullSearchHeight` |
285
+ | `onHeightChange` | `(height: number) => void` | Required - pass the height back as `pullSearchHeight` |
286
286
  | `value` | `string` | Controlled value |
287
287
  | `placeholder` | `string` | Placeholder text |
288
288
  | `debounce` | `boolean` | Debounce `onChangeText` by 500ms |
@@ -305,7 +305,7 @@ Pass `keyboardAware` to any scroll component (`ScrollView`, `FlatList`, `Section
305
305
  </ScrollViewProvider>
306
306
  ```
307
307
 
308
- By default, a fixed `ScrollViewFooter` doesn't follow the keyboard — it stays pinned to the screen bottom and the keyboard simply covers it. If the footer holds something the user needs while typing (a submit button, say), set `footerAboveKeyboard` on `ScrollViewProvider` so the footer floats above the keyboard instead, reachable without dismissing it first:
308
+ By default, a fixed `ScrollViewFooter` doesn't follow the keyboard - it stays pinned to the screen bottom and the keyboard simply covers it. If the footer holds something the user needs while typing (a submit button, say), set `footerAboveKeyboard` on `ScrollViewProvider` so the footer floats above the keyboard instead, reachable without dismissing it first:
309
309
 
310
310
  ```tsx
311
311
  <ScrollViewProvider footerFixed footerAboveKeyboard>
@@ -320,25 +320,25 @@ By default, a fixed `ScrollViewFooter` doesn't follow the keyboard — it stays
320
320
  </ScrollViewProvider>
321
321
  ```
322
322
 
323
- `footerAboveKeyboard` only has an effect when the footer is fixed (`footerFixed`) — a footer that scrolls away with content has nothing to reserve keyboard space for. Always pair it with `keyboardAware: true` on any scroll component that could have a focused input while it's visible. Content always reserves space for the current footer height regardless of `keyboardAware` (so a fixed footer never overlaps it) — and while floating, that height already includes the keyboard, so a list with `keyboardAware: false` ends up reserving keyboard space anyway, just indirectly through the footer rather than directly. There's no useful configuration where you'd want the footer reachable above the keyboard but *not* want that same list's own content clear of it too, so don't rely on `keyboardAware: false` to opt a list out of this while `footerAboveKeyboard` is on — it won't.
323
+ `footerAboveKeyboard` only has an effect when the footer is fixed (`footerFixed`) - a footer that scrolls away with content has nothing to reserve keyboard space for. Always pair it with `keyboardAware: true` on any scroll component that could have a focused input while it's visible. Content always reserves space for the current footer height regardless of `keyboardAware` (so a fixed footer never overlaps it) - and while floating, that height already includes the keyboard, so a list with `keyboardAware: false` ends up reserving keyboard space anyway, just indirectly through the footer rather than directly. There's no useful configuration where you'd want the footer reachable above the keyboard but *not* want that same list's own content clear of it too, so don't rely on `keyboardAware: false` to opt a list out of this while `footerAboveKeyboard` is on - it won't.
324
324
 
325
- Floating is done by growing the footer's own container rather than translating it, so its `BlurView` backdrop always stays sealed against the true physical screen bottom — nothing shows through beneath it, including at the keyboard's rounded top corners.
325
+ Floating is done by growing the footer's own container rather than translating it, so its `BlurView` backdrop always stays sealed against the true physical screen bottom - nothing shows through beneath it, including at the keyboard's rounded top corners.
326
326
 
327
- While floating above an open keyboard, the footer also drops its safe-area bottom inset — it's no longer sitting at the physical screen edge, so that padding would just be dead space under its content. As the keyboard closes, the inset ramps back in continuously over the last bit of its travel rather than snapping back once it's fully closed, so the content settles at the safe-area line smoothly instead of dropping to the physical bottom edge and correcting back up.
327
+ While floating above an open keyboard, the footer also drops its safe-area bottom inset - it's no longer sitting at the physical screen edge, so that padding would just be dead space under its content. As the keyboard closes, the inset ramps back in continuously over the last bit of its travel rather than snapping back once it's fully closed, so the content settles at the safe-area line smoothly instead of dropping to the physical bottom edge and correcting back up.
328
328
 
329
- Keyboard awareness is native-only — `useKeyboardInset` (and therefore `keyboardAware`/`footerAboveKeyboard`) always resolves to `0` height on web.
329
+ Keyboard awareness is native-only - `useKeyboardInset` (and therefore `keyboardAware`/`footerAboveKeyboard`) always resolves to `0` height on web.
330
330
 
331
331
  ---
332
332
 
333
333
  ## Persistent bottom chrome (hiding a tab bar with scroll)
334
334
 
335
- A `ScrollViewFooter` hides on scroll, but an app's tab bar lives outside this package — it stays put, and the footer has to be lifted above it with `tabBarHeight`. `ScrollViewChromeProvider` turns the two into one rigid stack: the screen's footer and the tab bar share a single animated offset, so they hide and reveal together as one piece, frame for frame.
335
+ A `ScrollViewFooter` hides on scroll, but an app's tab bar lives outside this package - it stays put, and the footer has to be lifted above it with `tabBarHeight`. `ScrollViewChromeProvider` turns the two into one rigid stack: the screen's footer and the tab bar share a single animated offset, so they hide and reveal together as one piece, frame for frame.
336
336
 
337
337
  Three pieces make it work:
338
338
 
339
- 1. **Host** — mount `ScrollViewChromeProvider` above the navigator, giving it the bar's `footprint`.
340
- 2. **Screens** — nothing to change. Each `ScrollViewProvider` (and its `ScrollViewHeader`, `ScrollViewFooter`, scroll component) below the host joins it while its screen is focused.
341
- 3. **Bar** — your own tab bar reads the shared offset with `useScrollViewChromeStyle()` and applies it as an animated style. It has to overlay the screen (absolutely positioned at the bottom) with a resting box exactly `footprint` tall, since hosted footers are lifted by that number.
339
+ 1. **Host** - mount `ScrollViewChromeProvider` above the navigator, giving it the bar's `footprint`.
340
+ 2. **Screens** - nothing to change. Each `ScrollViewProvider` (and its `ScrollViewHeader`, `ScrollViewFooter`, scroll component) below the host joins it while its screen is focused.
341
+ 3. **Bar** - your own tab bar reads the shared offset with `useScrollViewChromeStyle()` and applies it as an animated style. It has to overlay the screen (absolutely positioned at the bottom) with a resting box exactly `footprint` tall, since hosted footers are lifted by that number.
342
342
 
343
343
  ```tsx
344
344
  // app/(tabs)/_layout.tsx
@@ -389,7 +389,7 @@ const styles = StyleSheet.create({
389
389
  })
390
390
  ```
391
391
 
392
- Screens are written exactly as they were — there is no chrome-specific prop on the happy path:
392
+ Screens are written exactly as they were - there is no chrome-specific prop on the happy path:
393
393
 
394
394
  ```tsx
395
395
  // app/(tabs)/library.tsx
@@ -408,7 +408,7 @@ export default function Library() {
408
408
 
409
409
  ### Pinning the bar
410
410
 
411
- `useScrollViewChromePin(active)` holds the bar fully revealed — and stops scroll from hiding it — for as long as `active` is `true`. Use it whenever something on screen needs the bar to stay put, such as a focused search field or a multi-select in progress. Pins are counted, so two independent pinners can't release each other, and it works from any component under the host (below a `ScrollViewProvider` too).
411
+ `useScrollViewChromePin(active)` holds the bar fully revealed - and stops scroll from hiding it - for as long as `active` is `true`. Use it whenever something on screen needs the bar to stay put, such as a focused search field or a multi-select in progress. Pins are counted, so two independent pinners can't release each other, and it works from any component under the host (below a `ScrollViewProvider` too).
412
412
 
413
413
  ```tsx
414
414
  function SearchField() {
@@ -418,11 +418,11 @@ function SearchField() {
418
418
  }
419
419
  ```
420
420
 
421
- `useScrollViewChromeReveal()` returns a function that animates the bar back to fully revealed (200ms) — handy for a tab press or a "scroll to top" action, as in the example above.
421
+ `useScrollViewChromeReveal()` returns a function that animates the bar back to fully revealed (200ms) - handy for a tab press or a "scroll to top" action, as in the example above.
422
422
 
423
423
  ### Nested providers that aren't screens
424
424
 
425
- A `ScrollViewProvider` nested inside another one never sees the host: every hosted provider clears it for its own children, so a panel inside a screen can't compete with its parent. A picker or form modal is the exception — a portal can re-parent it out from under its screen's provider, and it would then claim the host and drive the bar from its own scrolling. Pass `chrome={false}` to opt those out:
425
+ A `ScrollViewProvider` nested inside another one never sees the host: every hosted provider clears it for its own children, so a panel inside a screen can't compete with its parent. A picker or form modal is the exception - a portal can re-parent it out from under its screen's provider, and it would then claim the host and drive the bar from its own scrolling. Pass `chrome={false}` to opt those out:
426
426
 
427
427
  ```tsx
428
428
  <ScrollViewProvider chrome={false} footerFixed>
@@ -435,18 +435,18 @@ A `ScrollViewProvider` nested inside another one never sees the host: every host
435
435
 
436
436
  ### What changes for a hosted screen
437
437
 
438
- - **The footer joins the stack.** It sits `footprint` above the screen bottom, translates by the shared offset (clamped to the full hide distance), and pads its bottom by `overhang` instead of the safe-area inset — the bar's `footprint` already includes that inset.
438
+ - **The footer joins the stack.** It sits `footprint` above the screen bottom, translates by the shared offset (clamped to the full hide distance), and pads its bottom by `overhang` instead of the safe-area inset - the bar's `footprint` already includes that inset.
439
439
  - **Hide distance.** The stack slides `footprint + max(footerHeight, overhang) + 2` points: the bar, plus the screen's footer (whose measured height already includes the `overhang` padding; with no footer, just the `overhang`), plus 2 points of slack so a soft shadow bleeding past the bar's box doesn't leave a sliver at the screen edge.
440
440
  - **Footer snap-back is forced.** A hosted footer follows the host's `snapBack` (default `true`) whatever the provider, `snapBackFooter` or settings values say, so the stack reveals on a short scroll-up from anywhere in a list. The header's own snap-back is untouched.
441
- - **Scrolling stops at an end.** Hiding tracks the scroll 1:1, so a drag can end with the stack half hidden. When a drag ends without momentum, or momentum ends, the stack animates (200ms) to whichever end is nearer — fully shown or fully hidden, never cut off.
441
+ - **Scrolling stops at an end.** Hiding tracks the scroll 1:1, so a drag can end with the stack half hidden. When a drag ends without momentum, or momentum ends, the stack animates (200ms) to whichever end is nearer - fully shown or fully hidden, never cut off.
442
442
  - **Short lists never hide.** A list that can't scroll at least the hide distance would strand the stack half hidden, so it never starts hiding and stays revealed.
443
443
  - **Footer Lock keeps it revealed.** A fixed footer (`footerFixed`, `fixed`, or the settings default) stops the scroll handlers from writing the offset, and turning it on while the stack is hidden animates the stack back. The hosted footer still reads the shared offset rather than pinning itself to zero, so the footer and the bar stay equal on every frame, including that animation.
444
- - **The focused screen owns the bar.** Screens that are mounted but unfocused never write the offset. Whenever ownership changes — switching tabs, or landing on a screen with no `ScrollViewProvider` at all — the stack resets to fully revealed instead of staying wherever the last screen parked it. A pin engaging does the same.
445
- - **The reserved bottom inset stays constant while hidden.** Scroll content reserves `footprint` plus the overhang (or a fixed footer's height) at the bottom. That space can't animate, so it stays reserved as the bar slides away rather than the list resizing mid-scroll — the last row can always scroll clear of a revealed bar, and nothing shifts when the bar hides.
444
+ - **The focused screen owns the bar.** Screens that are mounted but unfocused never write the offset. Whenever ownership changes - switching tabs, or landing on a screen with no `ScrollViewProvider` at all - the stack resets to fully revealed instead of staying wherever the last screen parked it. A pin engaging does the same.
445
+ - **The reserved bottom inset stays constant while hidden.** Scroll content reserves `footprint` plus the overhang (or a fixed footer's height) at the bottom. That space can't animate, so it stays reserved as the bar slides away rather than the list resizing mid-scroll - the last row can always scroll clear of a revealed bar, and nothing shifts when the bar hides.
446
446
  - **`tabBarHeight` is ignored.** The host's `footprint` is the same "reserve this much at the bottom" number, just known exactly, and takes its place.
447
447
  - **With a fixed footer floating above the keyboard** (`footerFixed` + `footerAboveKeyboard`), the footer sits above whichever is taller, the bar or the keyboard, instead of floating a whole bar-height above the keyboard.
448
448
 
449
- Outside a host — or in a provider with `chrome={false}` — none of this applies and every behavior in the rest of this README is unchanged. `useScrollViewChromeStyle`, `useScrollViewChromePin` and `useScrollViewChromeReveal` are also inert there (a permanent zero translate, a no-op pin and a no-op reveal), so a shared tab bar component can call them unconditionally.
449
+ Outside a host - or in a provider with `chrome={false}` - none of this applies and every behavior in the rest of this README is unchanged. `useScrollViewChromeStyle`, `useScrollViewChromePin` and `useScrollViewChromeReveal` are also inert there (a permanent zero translate, a no-op pin and a no-op reveal), so a shared tab bar component can call them unconditionally.
450
450
 
451
451
  ---
452
452
 
@@ -465,7 +465,7 @@ const { scrollPosition, scrollHeight, progressing, progress, setProgress, setPro
465
465
  | `scrollPosition` | `SharedValue<number>` | Live scroll offset (Reanimated worklet-safe) |
466
466
  | `scrollHeight` | `number` | Visible scroll area height (window minus header/footer) |
467
467
  | `progressing` | `boolean` | Whether the header progress bar is visible |
468
- | `progress` | `number \| null` | Progress bar value (0–1), or `null` for indeterminate |
468
+ | `progress` | `number \| null` | Progress bar value (0-1), or `null` for indeterminate |
469
469
  | `setProgress` | `(p: number \| null) => void` | Set progress; pass `null` for indeterminate |
470
470
  | `setProgressing` | `(b: boolean) => void` | Show/hide the progress bar |
471
471
 
@@ -567,8 +567,8 @@ store.dispatch(scrollViewActions.initialize({ snapBack: true, headerFixed: false
567
567
  | `headerFixed` | `false` | Pin header globally |
568
568
  | `footerFixed` | `false` | Pin footer globally |
569
569
  | `snapBack` | `false` | Snap header and footer back when scrolling up |
570
- | `snapBackHeader` | — | Override `snapBack` for header only |
571
- | `snapBackFooter` | — | Override `snapBack` for footer only |
570
+ | `snapBackHeader` | - | Override `snapBack` for header only |
571
+ | `snapBackFooter` | - | Override `snapBack` for footer only |
572
572
  | `backActionFixed` | `true` | Keep back button visible as header scrolls away |
573
573
 
574
574
  Settings cascade: `ScrollViewSettingsProvider` → `ScrollViewProvider` → individual scroll component props. More specific values always win.
package/dist/index.js CHANGED
@@ -956,7 +956,7 @@ var PullSearch = (0, import_react10.forwardRef)(function PullSearch2({ debounce,
956
956
  (isHidden, wasHidden) => {
957
957
  if (isHidden && !wasHidden) (0, import_react_native_reanimated9.runOnJS)(blurInput)();
958
958
  },
959
- // scrollPosition has to be listed too, not just read — see useStickyHeaders' useAnimatedReaction
959
+ // scrollPosition has to be listed too, not just read - see useStickyHeaders' useAnimatedReaction
960
960
  // for why: on web, without reanimated's Babel plugin active, an unlisted SharedValue mutation
961
961
  // never re-triggers this reaction (confirmed empirically elsewhere in this package).
962
962
  [barHeight, headerHeight, blurInput, scrollPosition]
@@ -1393,7 +1393,7 @@ var ScrollViewProvider = ({ blur, chrome, children, fixed = false, footerAboveKe
1393
1393
  (0, import_react17.useEffect)(() => {
1394
1394
  if (!__DEV__ || headerHeight !== null) return;
1395
1395
  const timeout = setTimeout(() => {
1396
- console.warn("[scroll-view] No ScrollViewHeader ever measured inside this ScrollViewProvider \u2014 scroll content stays hidden (opacity: 0) until one does. Render a <ScrollViewHeader> (it can be empty / zero height) inside this provider.");
1396
+ console.warn("[scroll-view] No ScrollViewHeader ever measured inside this ScrollViewProvider: scroll content stays hidden (opacity: 0) until one does. Render a <ScrollViewHeader> (it can be empty / zero height) inside this provider.");
1397
1397
  }, 3e3);
1398
1398
  return () => clearTimeout(timeout);
1399
1399
  }, [headerHeight]);
@@ -1486,7 +1486,7 @@ function useStickyHeaders(headerFixed, enabled) {
1486
1486
  // Same web-only reanimated gap as useAnimatedStyle elsewhere in this file: this "prepare"
1487
1487
  // function's own SharedValue reads (sectionYPositions, headerHeightShared, scrollPosition,
1488
1488
  // snapBackHeaderShared, headerOffset, pullSearchHeightShared) need to be listed explicitly too
1489
- // — confirmed empirically that without them, useAnimatedReaction re-evaluates once on mount and
1489
+ // - confirmed empirically that without them, useAnimatedReaction re-evaluates once on mount and
1490
1490
  // then never again on web, permanently freezing activeIndex/the active sticky section.
1491
1491
  [enabled, headerFixed, insetsTop, sectionYPositions, headerHeightShared, scrollPosition, snapBackHeaderShared, headerOffset, pullSearchHeightShared]
1492
1492
  );
package/dist/index.mjs CHANGED
@@ -900,7 +900,7 @@ var PullSearch = forwardRef(function PullSearch2({ debounce, onChangeText, onHei
900
900
  (isHidden, wasHidden) => {
901
901
  if (isHidden && !wasHidden) runOnJS3(blurInput)();
902
902
  },
903
- // scrollPosition has to be listed too, not just read — see useStickyHeaders' useAnimatedReaction
903
+ // scrollPosition has to be listed too, not just read - see useStickyHeaders' useAnimatedReaction
904
904
  // for why: on web, without reanimated's Babel plugin active, an unlisted SharedValue mutation
905
905
  // never re-triggers this reaction (confirmed empirically elsewhere in this package).
906
906
  [barHeight, headerHeight, blurInput, scrollPosition]
@@ -1337,7 +1337,7 @@ var ScrollViewProvider = ({ blur, chrome, children, fixed = false, footerAboveKe
1337
1337
  useEffect7(() => {
1338
1338
  if (!__DEV__ || headerHeight !== null) return;
1339
1339
  const timeout = setTimeout(() => {
1340
- console.warn("[scroll-view] No ScrollViewHeader ever measured inside this ScrollViewProvider \u2014 scroll content stays hidden (opacity: 0) until one does. Render a <ScrollViewHeader> (it can be empty / zero height) inside this provider.");
1340
+ console.warn("[scroll-view] No ScrollViewHeader ever measured inside this ScrollViewProvider: scroll content stays hidden (opacity: 0) until one does. Render a <ScrollViewHeader> (it can be empty / zero height) inside this provider.");
1341
1341
  }, 3e3);
1342
1342
  return () => clearTimeout(timeout);
1343
1343
  }, [headerHeight]);
@@ -1430,7 +1430,7 @@ function useStickyHeaders(headerFixed, enabled) {
1430
1430
  // Same web-only reanimated gap as useAnimatedStyle elsewhere in this file: this "prepare"
1431
1431
  // function's own SharedValue reads (sectionYPositions, headerHeightShared, scrollPosition,
1432
1432
  // snapBackHeaderShared, headerOffset, pullSearchHeightShared) need to be listed explicitly too
1433
- // — confirmed empirically that without them, useAnimatedReaction re-evaluates once on mount and
1433
+ // - confirmed empirically that without them, useAnimatedReaction re-evaluates once on mount and
1434
1434
  // then never again on web, permanently freezing activeIndex/the active sticky section.
1435
1435
  [enabled, headerFixed, insetsTop, sectionYPositions, headerHeightShared, scrollPosition, snapBackHeaderShared, headerOffset, pullSearchHeightShared]
1436
1436
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rific/scroll-view",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Blur styled scroll system for React Native. Floating headers/footers, keyboard awareness, and scroll-away chrome",
5
5
  "keywords": [
6
6
  "react-native",
@@ -62,11 +62,11 @@
62
62
  "prettier": "@infinitetoken/eslint-config/prettier",
63
63
  "devDependencies": {
64
64
  "@infinitetoken/eslint-config": "0.2.0",
65
- "@infinitetoken/jest-config": "^0.4.0",
66
- "@infinitetoken/tsconfig": "^0.5.0",
65
+ "@infinitetoken/jest-config": "^0.5.1",
66
+ "@infinitetoken/tsconfig": "^0.6.0",
67
67
  "@reduxjs/toolkit": "^2.12.0",
68
- "@rific/auto-paper": "^0.12.0",
69
- "@rific/core": "^0.2.0",
68
+ "@rific/auto-paper": "^0.13.3",
69
+ "@rific/core": "^0.3.1",
70
70
  "@testing-library/dom": "^10.4.1",
71
71
  "@testing-library/react": "^16.3.2",
72
72
  "@types/jest": "^30.0.0",
@@ -80,13 +80,13 @@
80
80
  "prettier": "^3.8.3",
81
81
  "react": "^19.0.0",
82
82
  "react-dom": "^19.0.0",
83
- "react-native": "^0.83.0",
84
- "react-native-gesture-handler": "^2.32.0",
83
+ "react-native": "^0.88.0",
84
+ "react-native-gesture-handler": "^3.3.0",
85
85
  "react-native-keyboard-controller": "*",
86
86
  "react-native-paper": "^5.12.0",
87
- "react-native-reanimated": "^4.5.1",
87
+ "react-native-reanimated": "^4.7.1",
88
88
  "react-native-safe-area-context": "*",
89
- "react-native-worklets": "^0.10.3",
89
+ "react-native-worklets": "^0.13.0",
90
90
  "tsup": "^8.0.0",
91
91
  "typescript": "^6.0.3"
92
92
  },
@@ -95,7 +95,7 @@
95
95
  "@rific/core": ">=0.1.1",
96
96
  "react": ">=19.0.0",
97
97
  "react-native": ">=0.83.0",
98
- "react-native-gesture-handler": ">=2.0.0 <3.0.0",
98
+ "react-native-gesture-handler": ">=2.0.0 <4.0.0",
99
99
  "react-native-keyboard-controller": ">=1.0.0",
100
100
  "react-native-paper": ">=5.0.0",
101
101
  "react-native-reanimated": ">=4.0.0",
@@ -38,13 +38,13 @@ const CustomListInner = <P extends object>({ chipProps, chipThreshold, component
38
38
 
39
39
  const { chipHidden, chipStyle, containerStyle, contentInset, contentOffset, contentPadding, footerFixed, headerFixed } = useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedProp, keyboardAware, pullSearchHeight, style })
40
40
  const { listGeneration, onListUnmount } = useContext(ScrollViewContext)
41
- // -1 never matches a real generation (starts at 0, only increments) — the runOnUI call below
41
+ // -1 never matches a real generation (starts at 0, only increments) - the runOnUI call below
42
42
  // corrects it before any scroll event could observe the placeholder.
43
43
  const capturedGeneration = useSharedValue(-1)
44
44
  // Layout effects (not passive effects): React guarantees all layout-effect cleanups for fibers
45
45
  // removed in a commit run before any layout-effect setups for fibers added in that same commit,
46
46
  // but gives no such ordering guarantee for passive effects. On a key-forced remount, the outgoing
47
- // instance's unmount and the incoming instance's mount land in the same commit — with passive
47
+ // instance's unmount and the incoming instance's mount land in the same commit - with passive
48
48
  // effects, the incoming instance's sync could run before the outgoing instance's bump, permanently
49
49
  // capturing a stale generation and silently disabling this instance's onScroll handling forever.
50
50
  // Correct ordering alone isn't sufficient, though: the onScroll worklet reads capturedGeneration
@@ -54,7 +54,7 @@ const CustomListInner = <P extends object>({ chipProps, chipThreshold, component
54
54
  // own serial queue, so it's guaranteed to run before any onScroll worklet invocation that could
55
55
  // only be queued after this component finishes mounting.
56
56
  //
57
- // Web has no such thread split, though — see ScrollView.tsx's own doc comment on this same
57
+ // Web has no such thread split, though - see ScrollView.tsx's own doc comment on this same
58
58
  // pattern for why runOnUI's deferred-to-next-frame behavior there leaves a real gap, and why a
59
59
  // synchronous assignment is the correct fix rather than a workaround.
60
60
  useLayoutEffect(() => {
package/src/FlatList.tsx CHANGED
@@ -30,9 +30,9 @@ export type FlatListProps<T> = Omit<RNFlatListProps<T>, 'onRefresh' | 'refreshin
30
30
 
31
31
  const FlatListInner = <T,>({ chipProps, chipThreshold, columnWrapperStyle, contentContainerStyle: externalContentContainerStyle, data, initialNumToRender = 20, keyExtractor, maxToRenderPerBatch = 50, numColumns, renderFilters, renderItem, footerFixed: footerFixedProp, gesture, headerFixed: headerFixedProp, horizontal, keyboardAware, ListHeaderComponent: externalListHeaderComponent, onChipPress, onMomentumScrollEnd: externalMomentumScrollEnd, onRefresh, onScrollBeginDrag: externalScrollBeginDrag, onScrollEndDrag: externalScrollEndDrag, pagingEnabled, pullSearchHeight, refreshing, removeClippedSubviews = false, showsHorizontalScrollIndicator, showsVerticalScrollIndicator, style, onContentSizeChange, ref: externalRef, windowSize = 100, ...props }: FlatListProps<T>) => {
32
32
  // Plain (non-Animated) FlatList: react-native-reanimated's Animated.FlatList does not reliably
33
- // honor the initial `contentOffset` prop on a key-forced remount — the header/list can briefly
33
+ // honor the initial `contentOffset` prop on a key-forced remount - the header/list can briefly
34
34
  // render at the wrong position. A plain FlatList honors it correctly, so scroll position is driven
35
- // by a regular JS onScroll callback (useScrollHandlerJS) instead of a Reanimated worklet — which
35
+ // by a regular JS onScroll callback (useScrollHandlerJS) instead of a Reanimated worklet - which
36
36
  // also avoids the "WorkletEventHandler" crash that a worklet handler triggers when attached to a
37
37
  // non-Animated component.
38
38
  const scrollView = useRef<RNFlatList<T>>(null)
@@ -42,10 +42,10 @@ const FlatListInner = <T,>({ chipProps, chipThreshold, columnWrapperStyle, conte
42
42
  // RN's own FlatList implements numColumns by chunking `data` into rows internally, but throws if
43
43
  // numColumns ever changes without the caller changing `key` to force a full remount. On iOS,
44
44
  // Fabric pools/recycles native ScrollView instances by component type across such a remount, and
45
- // the recycled instance's onScroll can silently stop firing for the new React instance entirely —
45
+ // the recycled instance's onScroll can silently stop firing for the new React instance entirely -
46
46
  // a native event-routing issue, not something a JS-side retry loop can detect or correct. Doing
47
47
  // the row-chunking ourselves here and always passing numColumns={1} through to the underlying
48
- // RNFlatList means numColumns never "changes" from RN's perspective — only `data`/`renderItem`
48
+ // RNFlatList means numColumns never "changes" from RN's perspective - only `data`/`renderItem`
49
49
  // change, which RNFlatList treats as an ordinary prop update with no remount at all, so callers
50
50
  // can toggle numColumns freely (e.g. switching a list to a grid) without ever hitting this class
51
51
  // of bug.
@@ -47,7 +47,7 @@ export const PullSearch = forwardRef<PullSearchHandle, PullSearchProps>(function
47
47
  (isHidden, wasHidden) => {
48
48
  if (isHidden && !wasHidden) runOnJS(blurInput)()
49
49
  },
50
- // scrollPosition has to be listed too, not just read — see useStickyHeaders' useAnimatedReaction
50
+ // scrollPosition has to be listed too, not just read - see useStickyHeaders' useAnimatedReaction
51
51
  // for why: on web, without reanimated's Babel plugin active, an unlisted SharedValue mutation
52
52
  // never re-triggers this reaction (confirmed empirically elsewhere in this package).
53
53
  [barHeight, headerHeight, blurInput, scrollPosition]
@@ -32,13 +32,13 @@ const ScrollViewInner = ({ chipProps, chipThreshold, children, contentContainerS
32
32
 
33
33
  const { chipHidden, chipStyle, containerStyle, contentInset, contentOffset, contentPadding, footerFixed, headerFixed } = useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedProp, isHorizontal, keyboardAware, pullSearchHeight, style })
34
34
  const { listGeneration, onListUnmount } = useContext(ScrollViewContext)
35
- // -1 never matches a real generation (starts at 0, only increments) — the runOnUI call below
35
+ // -1 never matches a real generation (starts at 0, only increments) - the runOnUI call below
36
36
  // corrects it before any scroll event could observe the placeholder.
37
37
  const capturedGeneration = useSharedValue(-1)
38
38
  // Layout effects (not passive effects): React guarantees all layout-effect cleanups for fibers
39
39
  // removed in a commit run before any layout-effect setups for fibers added in that same commit,
40
40
  // but gives no such ordering guarantee for passive effects. On a key-forced remount, the outgoing
41
- // instance's unmount and the incoming instance's mount land in the same commit — with passive
41
+ // instance's unmount and the incoming instance's mount land in the same commit - with passive
42
42
  // effects, the incoming instance's sync could run before the outgoing instance's bump, permanently
43
43
  // capturing a stale generation and silently disabling this instance's onScroll handling forever.
44
44
  // Correct ordering alone isn't sufficient, though: the onScroll worklet reads capturedGeneration
@@ -48,12 +48,12 @@ const ScrollViewInner = ({ chipProps, chipThreshold, children, contentContainerS
48
48
  // own serial queue, so it's guaranteed to run before any onScroll worklet invocation that could
49
49
  // only be queued after this component finishes mounting.
50
50
  //
51
- // Web has no such thread split, though — react-native-worklets' own runOnUI there (see its
51
+ // Web has no such thread split, though - react-native-worklets' own runOnUI there (see its
52
52
  // threads.js) only enqueues the callback for the NEXT requestAnimationFrame, it doesn't run it
53
53
  // synchronously. That opens exactly the gap the native path exists to close: a scroll event
54
54
  // (including useScrollInit's own mount-time imperative scrollTo) landing before that deferred
55
55
  // frame sees capturedGeneration still at its -1 placeholder, fails the generation check below,
56
- // and — since this effect only ever runs once per mount — never gets a second chance, silently
56
+ // and - since this effect only ever runs once per mount - never gets a second chance, silently
57
57
  // disabling scroll tracking (and therefore header/footer scroll-away) for this instance's entire
58
58
  // lifetime. Assigning directly is both simpler and provably correct on web: single-threaded JS
59
59
  // makes a synchronous write immediately visible to any read that follows it in program order, so
@@ -18,7 +18,7 @@ export type ScrollViewChromeProviderProps = {
18
18
  // their bottom by this instead of the safe-area inset, and the hide distance covers it.
19
19
  overhang?: number
20
20
  // Hosted footers reveal on a short scroll-up instead of only near the top. A bar that hides on
21
- // scroll has to be reachable from anywhere in a list, so this is on by default — and there is no
21
+ // scroll has to be reachable from anywhere in a list, so this is on by default - and there is no
22
22
  // "hide but only reveal near the top" mode: `snapBack={false}` turns hiding off entirely, leaving
23
23
  // the bar and hosted footers permanently revealed.
24
24
  snapBack?: boolean
@@ -30,7 +30,7 @@ export type ScrollViewChromeProviderProps = {
30
30
 
31
31
  // Mounted once above a navigator (a Tabs layout). Every ScrollViewProvider below it that is focused
32
32
  // claims it, and while it does, that screen's scroll handlers drive one shared `offset` that the
33
- // screen's own ScrollViewFooter AND the persistent bar both translate by — a single value read by
33
+ // screen's own ScrollViewFooter AND the persistent bar both translate by - a single value read by
34
34
  // both, so the two move as one rigid stack instead of being two animations that have to be kept in
35
35
  // step.
36
36
  export const ScrollViewChromeProvider = ({ children, footprint, overhang = 0, snapBack = true, useIsActive = alwaysActive }: ScrollViewChromeProviderProps) => {
@@ -23,7 +23,7 @@ export type ScrollViewContextType = {
23
23
  // Plain (non-Reanimated) counter for JS-thread-only scroll handlers (FlatList's useScrollHandlerJS).
24
24
  // A SharedValue is deliberately NOT used here: mutating `.value` from inside a React commit-phase
25
25
  // callback (a layout effect) is not synchronously visible even to an immediate subsequent read in
26
- // the same callback — Reanimated defers the actual flush — which silently broke the "did another
26
+ // the same callback - Reanimated defers the actual flush - which silently broke the "did another
27
27
  // list instance just unmount" check this counter exists for. A plain ref has no such batching.
28
28
  jsListGeneration: RefObject<number>
29
29
  listGeneration: SharedValue<number>
@@ -18,7 +18,7 @@ export const ScrollViewFooter = ({ children, style }: ScrollViewFooterProps) =>
18
18
  const keyboardHeight = useKeyboardInset()
19
19
  // setFooterHeight used to change identity on every real measurement (ScrollViewProvider's
20
20
  // useCallback closed over `footerHeight` itself). Depending on it directly here re-ran this
21
- // effect on every measurement too — tearing down the PREVIOUS closure as a "cleanup" and calling
21
+ // effect on every measurement too - tearing down the PREVIOUS closure as a "cleanup" and calling
22
22
  // ITS setFooterHeight(null) right after the real height had just been set, permanently clobbering
23
23
  // it back to null with no further layout event left to recover it (the DOM node's true size never
24
24
  // changed, so onLayout never fires again). The provider's setter is stable now, but the context
@@ -46,11 +46,11 @@ export const ScrollViewFooter = ({ children, style }: ScrollViewFooterProps) =>
46
46
  }
47
47
  // See ScrollViewHeader's translateStyle/blurStyle/progressStyle for why every SharedValue read
48
48
  // here (scrollPosition, headerHeightShared, pullSearchHeightShared, snapBackFooterShared,
49
- // footerOffset) also has to be listed explicitly — on web, reanimated doesn't pick up their
49
+ // footerOffset) also has to be listed explicitly - on web, reanimated doesn't pick up their
50
50
  // mutations as reactive triggers unless they're in this array, unlike native.
51
51
  const footerStyle = useAnimatedStyle(() => {
52
52
  // Hosted, the offset is the ONE value the persistent bar translates by too, so it is always
53
- // read straight through — never short-circuited to 0 for footerFixed the way the unhosted path
53
+ // read straight through - never short-circuited to 0 for footerFixed the way the unhosted path
54
54
  // below is. Footer Lock reaches it by animating the shared offset back to 0 instead (see
55
55
  // ChromeRegistrar), which keeps this footer and the bar equal on every frame, including the
56
56
  // ones where one would otherwise jump and the other animate.
@@ -62,7 +62,7 @@ export const ScrollViewFooter = ({ children, style }: ScrollViewFooterProps) =>
62
62
  return { transform: [{ translateY: Math.min(effective, footerHeight ?? 0) }] }
63
63
  }, [chromeHosted, footerHeight, footerFixed, headerHeightShared, pullSearchHeightShared, scrollPosition, snapBackFooterShared, footerOffset, stackHeightShared])
64
64
  // Floating above the keyboard is done by GROWING this container (via paddingBottom) rather than
65
- // translating it. Translating the whole bar left its bottom edge — and the BlurView filling it —
65
+ // translating it. Translating the whole bar left its bottom edge - and the BlurView filling it -
66
66
  // floating above the true screen bottom, exposing a gap of whatever sits behind (visible as a
67
67
  // strip above the keyboard, and worse at the keyboard's rounded top corners, which dip below its
68
68
  // reported height). Growing the container instead keeps its `bottom: 0` anchor untouched: the
@@ -71,7 +71,7 @@ export const ScrollViewFooter = ({ children, style }: ScrollViewFooterProps) =>
71
71
  // as the old translateY did for it.
72
72
  const containerPaddingBottom = footerFixed && footerAboveKeyboard ? keyboardHeight : 0
73
73
  // Web only, same as ScrollViewHeader's pre-paint measurement: react-native-web's onLayout lands
74
- // well after the first paint (a ResizeObserver callback plus a setTimeout measure — over a second
74
+ // well after the first paint (a ResizeObserver callback plus a setTimeout measure - over a second
75
75
  // on a cold load), and until footerHeight is known this footer draws no backdrop (see the
76
76
  // BlurView gate below) and the list reserves no room for it, so the row floated bare over the
77
77
  // content and its bar only appeared once the measurement arrived. Measuring the row here, after
@@ -90,13 +90,13 @@ export const ScrollViewFooter = ({ children, style }: ScrollViewFooterProps) =>
90
90
  }, [])
91
91
  // The safe-area bottom inset exists to clear the home indicator at the physical screen edge.
92
92
  // Once footerAboveKeyboard has floated this bar's content up above an open keyboard, it's no
93
- // longer sitting at that edge — the keyboard itself now occupies that space — so the inset would
93
+ // longer sitting at that edge - the keyboard itself now occupies that space - so the inset would
94
94
  // just be dead space under the content (e.g. under a Save button) for no reason.
95
95
  //
96
96
  // This has to ramp continuously with keyboardHeight, not flip as a boolean once the keyboard
97
97
  // fully closes: containerPaddingBottom above already tracks keyboardHeight every frame, so a
98
98
  // step-function padding change here lands in the same frame keyboardHeight hits 0, growing this
99
- // row by insets.bottom right as the container finishes shrinking back down — the content visually
99
+ // row by insets.bottom right as the container finishes shrinking back down - the content visually
100
100
  // overshoots to the physical bottom edge and then snaps back up. Ramping this padding by the same
101
101
  // keyboardHeight keeps the content's on-screen position moving continuously throughout: for
102
102
  // keyboardHeight >= insets.bottom the padding is 0 (unchanged from mid-close), and for the last
@@ -105,7 +105,7 @@ export const ScrollViewFooter = ({ children, style }: ScrollViewFooterProps) =>
105
105
  // of dropping past it and correcting back up.
106
106
  //
107
107
  // Hosted, the bar (which already includes the safe-area inset in its own footprint) sits below
108
- // this footer, so the row's bottom padding is only what pokes above the bar (a FAB) — not the
108
+ // this footer, so the row's bottom padding is only what pokes above the bar (a FAB) - not the
109
109
  // inset a second time.
110
110
  const bottomPadding = chromeHosted ? chromeOverhang : insets.bottom
111
111
  const rowPaddingBottom = footerFixed && footerAboveKeyboard ? Math.max(bottomPadding - keyboardHeight, 0) : bottomPadding
@@ -116,7 +116,7 @@ export const ScrollViewFooter = ({ children, style }: ScrollViewFooterProps) =>
116
116
  const footerBottom = chromeHosted && footerFixed && footerAboveKeyboard ? Math.max(tabBarHeight - keyboardHeight, 0) : tabBarHeight
117
117
  // A consumer's own `style.paddingBottom` (e.g. tightening the row's vertical rhythm to match a
118
118
  // header's actionMargin) is spread after rowPaddingBottom below, so it has to be added to it
119
- // rather than assigned after it — plain style-array merge would otherwise let it silently
119
+ // rather than assigned after it - plain style-array merge would otherwise let it silently
120
120
  // replace the safe-area clearance instead of sitting on top of it, leaving the row's real
121
121
  // content (buttons, etc.) flush against the home indicator.
122
122
  const { paddingBottom: consumerPaddingBottom, ...restStyle } = style ?? {}
@@ -12,12 +12,12 @@ export type ScrollViewHeaderProps = {
12
12
  actionSize?: number
13
13
  actionStyle?: ViewStyle
14
14
  // A plain callback renders the default Appbar.BackAction (themed onSurface, sized/positioned by
15
- // actionSize/iconSize below) — the common case, unchanged from before. Passing a ReactNode
15
+ // actionSize/iconSize below) - the common case, unchanged from before. Passing a ReactNode
16
16
  // instead skips that default entirely and renders the given element in its place, same escape
17
17
  // hatch trailingAction already gave the other corner: a caller that wants its own color, icon,
18
18
  // or component altogether (a colored IconButton, say) isn't stuck fighting Appbar.BackAction's
19
19
  // own fixed color/no-customization API to get it. backActionAccessibilityLabel/iconSize below
20
- // only apply to the default-rendered case — a custom element carries its own.
20
+ // only apply to the default-rendered case - a custom element carries its own.
21
21
  backAction?: (() => void) | ReactNode
22
22
  backActionAccessibilityLabel?: string
23
23
  backActionFixed?: boolean
@@ -56,7 +56,7 @@ export const ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, bac
56
56
  const top = topInset ? insets.top : 0
57
57
  // Tracks "has this header's own onLayout fired yet" as a SharedValue rather than deriving it from
58
58
  // headerHeight (a plain, React-state-backed number) so the worklets below never have to list a
59
- // value that flips from null to a real measured number in their dependency arrays — see the
59
+ // value that flips from null to a real measured number in their dependency arrays - see the
60
60
  // comment on translateStyle/blurStyle/progressStyle for why that recreation was a real crash, not
61
61
  // just a perf concern.
62
62
  const measuredShared = useSharedValue(false)
@@ -70,7 +70,7 @@ export const ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, bac
70
70
  }
71
71
  // Web only: measure synchronously before the first paint instead of waiting for onLayout.
72
72
  // react-native-web delivers onLayout from a ResizeObserver callback plus a setTimeout measure,
73
- // which routinely lands after ScrollViewProvider's two-frame "no header, assume 0" fallback —
73
+ // which routinely lands after ScrollViewProvider's two-frame "no header, assume 0" fallback -
74
74
  // so the first frames painted headerHeight === 0: header absolute with no backdrop, list with no
75
75
  // top padding, its first rows (sort/filter chips) drawn up inside the header until the real
76
76
  // height arrived. A layout effect runs after the DOM is built but before the browser paints, and
@@ -93,20 +93,20 @@ export const ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, bac
93
93
  const contentMinHeight = actionSize + 2 * actionMargin
94
94
  // Same natural height the header renders at in-flow before headerHeight is ever measured (see
95
95
  // useScrollList's containerStyle comment for that same "in-flow first, absolute once measured"
96
- // pattern) — used as the blur backdrop's own height for that same pre-measurement window, so it
96
+ // pattern) - used as the blur backdrop's own height for that same pre-measurement window, so it
97
97
  // doesn't render as a flat, unblurred box for the ~60-80ms an onLayout round trip takes on web.
98
98
  const unmeasuredHeight = top + contentMinHeight
99
99
  // Every one of these worklets reads SharedValues only (scrollPosition, snapBackHeaderShared,
100
- // headerOffset, pullSearchHeightShared, headerHeightShared, measuredShared) — deliberately NOT the
100
+ // headerOffset, pullSearchHeightShared, headerHeightShared, measuredShared) - deliberately NOT the
101
101
  // plain `headerHeight` context value, even though it holds the same number. `headerHeight` is a
102
102
  // React-state-backed `number | null` that changes identity on every measurement (starting at null,
103
103
  // then flipping to a real pixel value within a frame of mount); putting it in a useAnimatedStyle
104
104
  // dependency array forces Reanimated to tear down and recreate the worklet the moment that happens
105
- // — which crashed outright ("[Worklets] Tried to synchronously call a Remote Function") on
105
+ // - which crashed outright ("[Worklets] Tried to synchronously call a Remote Function") on
106
106
  // react-native-worklets 0.10.1. SharedValues never change identity, only their `.value`, so they're
107
107
  // safe to depend on and don't retrigger this. They still have to be LISTED here despite that: on
108
108
  // web, without the Reanimated Babel plugin's automatic worklet dependency capture, a SharedValue's
109
- // mutations aren't picked up as reactive unless its reference appears in this array — omitting one
109
+ // mutations aren't picked up as reactive unless its reference appears in this array - omitting one
110
110
  // silently freezes that value's contribution to the header/blur/progress-bar's scroll tracking at
111
111
  // whatever it was when the array last changed.
112
112
  const translateStyle = useAnimatedStyle(() => {
@@ -12,7 +12,7 @@ import { ScrollViewSettingsContext } from './ScrollViewSettingsContext'
12
12
  export type ScrollViewProviderProps = {
13
13
  blur?: boolean
14
14
  // Opt out of an enclosing ScrollViewChromeProvider. A screen inside a hosted navigator joins the
15
- // host by default; a nested provider that is NOT a screen (a picker or form modal — Paper's Portal
15
+ // host by default; a nested provider that is NOT a screen (a picker or form modal - Paper's Portal
16
16
  // can re-parent those above the clearing this provider does for its own children) has to say so.
17
17
  chrome?: boolean
18
18
  children: ReactNode
@@ -58,13 +58,13 @@ export const ScrollViewProvider = ({ blur, chrome, children, fixed = false, foot
58
58
  const chromeWritable = useSharedValue(!chromeHosted)
59
59
  // Only the registrar ever writes this while hosted. If a provider stops being hosted while it stays
60
60
  // mounted (chrome flipping to false, the host unmounting), the registrar goes away with it and
61
- // would leave the last value behind — possibly false, which would silently switch off this
61
+ // would leave the last value behind - possibly false, which would silently switch off this
62
62
  // provider's own snap-back footer for good.
63
63
  useEffect(() => {
64
64
  if (!chromeHosted) chromeWritable.value = true
65
65
  }, [chromeHosted, chromeWritable])
66
66
  const revealChrome = chromeHost?.reveal
67
- // Unhosted, the hide distance is simply the footer's own height — the SAME SharedValue, not a
67
+ // Unhosted, the hide distance is simply the footer's own height - the SAME SharedValue, not a
68
68
  // copy, so nothing about existing snap-back behavior changes. Hosted, the whole stack (screen
69
69
  // footer + bar + whatever pokes above the bar) has to clear the screen, and the footer's own
70
70
  // measured height already includes the overhang as its bottom padding.
@@ -96,7 +96,7 @@ export const ScrollViewProvider = ({ blur, chrome, children, fixed = false, foot
96
96
  // react-native-screens' web shim hides an inactive screen via display:none on an
97
97
  // ancestor instead of unmounting it, so the header's ResizeObserver-backed onLayout
98
98
  // keeps firing while hidden and reports a spurious 0 (no layout box), then again once
99
- // revealed. Never let a spurious 0 clobber a real height — only a true remount
99
+ // revealed. Never let a spurious 0 clobber a real height - only a true remount
100
100
  // (headerHeight reset to null) should ever legitimately shrink it back to 0.
101
101
  if (h === 0 && headerHeightRef.current) return
102
102
  headerHeightRef.current = h
@@ -105,7 +105,7 @@ export const ScrollViewProvider = ({ blur, chrome, children, fixed = false, foot
105
105
  // Mirrored into the SharedValue in the same commit as the state, never ahead of it. Writing it
106
106
  // straight from onLayout let the header's translate worklet see the new height while the list
107
107
  // still had no top padding and scrollPosition hadn't been moved to its resting -headerHeight
108
- // (useScrollInit does that in its own layout effect on this commit) — so for the whole re-render
108
+ // (useScrollInit does that in its own layout effect on this commit) - so for the whole re-render
109
109
  // (~50ms on web) the header computed translateY = -headerHeight and slid off-screen, a visible
110
110
  // flash of a header-less screen with the list's first rows at the very top.
111
111
  useLayoutEffect(() => {
@@ -125,14 +125,14 @@ export const ScrollViewProvider = ({ blur, chrome, children, fixed = false, foot
125
125
  if (!__DEV__ || headerHeight !== null) return
126
126
  const timeout = setTimeout(() => {
127
127
  // eslint-disable-next-line no-console
128
- console.warn('[scroll-view] No ScrollViewHeader ever measured inside this ScrollViewProvider — scroll content stays hidden (opacity: 0) until one does. Render a <ScrollViewHeader> (it can be empty / zero height) inside this provider.')
128
+ console.warn('[scroll-view] No ScrollViewHeader ever measured inside this ScrollViewProvider: scroll content stays hidden (opacity: 0) until one does. Render a <ScrollViewHeader> (it can be empty / zero height) inside this provider.')
129
129
  }, 3000)
130
130
  return () => clearTimeout(timeout)
131
131
  }, [headerHeight])
132
132
  // Web-only recovery for a real race: a header's ResizeObserver-backed onLayout can simply never
133
- // fire on the very first mount through Expo Router (observed in practice, not just theorized —
133
+ // fire on the very first mount through Expo Router (observed in practice, not just theorized -
134
134
  // content stuck at opacity: 0 indefinitely, with no error, only escaping via an unrelated resize
135
- // forcing a fresh layout pass). Native RN doesn't have this race — onLayout is reliable there, and
135
+ // forcing a fresh layout pass). Native RN doesn't have this race - onLayout is reliable there, and
136
136
  // a persistently-null headerHeight really does mean "no header was ever rendered," which the
137
137
  // warning above should keep flagging. On web, give a real first layout two frames to arrive before
138
138
  // assuming there isn't one; setHeaderHeight(0) is exactly what a genuinely header-less screen would
@@ -154,7 +154,7 @@ export const ScrollViewProvider = ({ blur, chrome, children, fixed = false, foot
154
154
  // shim (and a footer's own unmount cleanup below, which now passes null instead of 0 for
155
155
  // exactly this reason) can report/force a spurious 0 while a real footer is effectively
156
156
  // still on screen (e.g. a wizard cycling through several <ScrollViewFooter> instances).
157
- // Never let that clobber a known height — only an explicit null (genuinely no footer
157
+ // Never let that clobber a known height - only an explicit null (genuinely no footer
158
158
  // mounted) may reset it.
159
159
  if (h === 0 && footerHeightRef.current) return
160
160
  footerHeightRef.current = h
@@ -36,13 +36,13 @@ const SectionListInner = <ItemT, SectionT extends { data: ReadonlyArray<ItemT> }
36
36
 
37
37
  const { chipHidden, chipStyle, containerStyle, contentInset, contentOffset, contentPadding, footerFixed, headerFixed, headerHeight } = useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedProp, hideUntilMeasured: true, keyboardAware, pullSearchHeight, style })
38
38
  const { listGeneration, onListUnmount } = useContext(ScrollViewContext)
39
- // -1 never matches a real generation (starts at 0, only increments) — the runOnUI call below
39
+ // -1 never matches a real generation (starts at 0, only increments) - the runOnUI call below
40
40
  // corrects it before any scroll event could observe the placeholder.
41
41
  const capturedGeneration = useSharedValue(-1)
42
42
  // Layout effect (not passive effect): React guarantees all layout-effect cleanups for fibers
43
43
  // removed in a commit run before any layout-effect setups for fibers added in that same commit,
44
44
  // but gives no such ordering guarantee for passive effects. On a key-forced remount, the outgoing
45
- // instance's unmount and the incoming instance's mount land in the same commit — with passive
45
+ // instance's unmount and the incoming instance's mount land in the same commit - with passive
46
46
  // effects, the incoming instance's sync could run before the outgoing instance's bump, permanently
47
47
  // capturing a stale generation and silently disabling this instance's onScroll handling forever.
48
48
  // Correct ordering alone isn't sufficient, though: the onScroll worklet reads capturedGeneration
@@ -52,7 +52,7 @@ const SectionListInner = <ItemT, SectionT extends { data: ReadonlyArray<ItemT> }
52
52
  // own serial queue, so it's guaranteed to run before any onScroll worklet invocation that could
53
53
  // only be queued after this component finishes mounting.
54
54
  //
55
- // Web has no such thread split, though — see ScrollView.tsx's own doc comment on this same
55
+ // Web has no such thread split, though - see ScrollView.tsx's own doc comment on this same
56
56
  // pattern for why runOnUI's deferred-to-next-frame behavior there leaves a real gap, and why a
57
57
  // synchronous assignment is the correct fix rather than a workaround.
58
58
  useLayoutEffect(() => {
@@ -71,7 +71,7 @@ const SectionListInner = <ItemT, SectionT extends { data: ReadonlyArray<ItemT> }
71
71
  const { activeIndex, clipStyle, measureHeader, overlayStyle, resetPositions } = useStickyHeaders(headerFixed, usesCustomSticky)
72
72
 
73
73
  // useLayoutEffect (not useEffect) so the reset runs before native layout fires onLayout
74
- // for the newly mounted section headers — otherwise the reset would clear correct positions.
74
+ // for the newly mounted section headers - otherwise the reset would clear correct positions.
75
75
  useLayoutEffect(() => {
76
76
  if (usesCustomSticky) resetPositions(sections.length)
77
77
  }, [sections, usesCustomSticky, resetPositions, headerHeight])
@@ -11,7 +11,7 @@ type ChromeRegistrarProps = {
11
11
 
12
12
  // Rendered by a hosted ScrollViewProvider as a sibling of its children. It is a component rather
13
13
  // than a hook call inside the provider because host.useIsActive is itself a hook, and a provider
14
- // only sometimes has a host — mounting this only when hosted keeps the hook order stable.
14
+ // only sometimes has a host - mounting this only when hosted keeps the hook order stable.
15
15
  //
16
16
  // While its screen is active it claims the host; the provider's scroll handlers only write the
17
17
  // shared offset (`writable`) while this provider is the claim's owner and nothing has pinned the
@@ -31,7 +31,7 @@ type DotProps = {
31
31
  const Dot = ({ activeColor, index, inactiveColor, pageWidth, scrollPosition }: DotProps) => {
32
32
  // Explicit array required on web even though nothing here looks platform-specific: without
33
33
  // reanimated's Babel plugin active for the web bundle, useAnimatedStyle has no way to
34
- // auto-extract dependencies and throws outright rather than silently under-tracking — omitting
34
+ // auto-extract dependencies and throws outright rather than silently under-tracking - omitting
35
35
  // this array isn't a lesser version of the header/footer bug elsewhere in this package, it's a
36
36
  // hard crash (confirmed live on the horizontal-pager demo).
37
37
  const circleStyle = useAnimatedStyle(() => {
@@ -4,11 +4,11 @@ import { StyleSheet, type ViewStyle } from 'react-native'
4
4
  import { Icon, useTheme } from 'react-native-paper'
5
5
  import Animated, { type AnimatedStyle } from 'react-native-reanimated'
6
6
 
7
- // 'children' omitted alongside the rest — this chip's own text always comes from `label` (or the
7
+ // 'children' omitted alongside the rest - this chip's own text always comes from `label` (or the
8
8
  // isHorizontal-driven 'Start'/'Top' default below), passed as the real JSX children the underlying
9
9
  // Chip renders with, which is what actually wins: React always takes the children an element is
10
10
  // given between its tags over one merely passed as a "children" prop via spread. A `children` in
11
- // chipProps would therefore type-check but silently do nothing at runtime — omitting it here means
11
+ // chipProps would therefore type-check but silently do nothing at runtime - omitting it here means
12
12
  // TypeScript catches that mismatch instead of a caller finding out the hard way.
13
13
  export type ChipProps = Omit<AutoPaperChipProps, 'compact' | 'icon' | 'onPress' | 'children'> & {
14
14
  label?: ReactNode
@@ -34,7 +34,7 @@ export const ScrollViewChip = ({ chipProps, isHorizontal, onChipPress, onPress,
34
34
  // Rendered via `avatar`, not Chip's own `icon` prop: react-native-paper's MD3 Chip hardcodes
35
35
  // `icon`'s color to theme.colors.primary and ignores `selectedColor` entirely (its own doc
36
36
  // comment: "With theme version 3 selectedColor doesn't apply to the icon. If you want [a]
37
- // custom color for the icon, render your own Icon component") — a chip whose own background
37
+ // custom color for the icon, render your own Icon component") - a chip whose own background
38
38
  // IS theme.colors.primary (this library's callers all set that; see Hangman's own vivid-chip
39
39
  // convention) renders that icon invisible against itself. `avatar` has no such override; it
40
40
  // clones whatever element/color it's given. Falls back to the same default Paper itself would
@@ -33,17 +33,17 @@ export type ScrollHandlerJS = {
33
33
  // Plain JS-thread scroll handler for a non-Animated scrollable. Reanimated shared values are
34
34
  // readable/writable from any thread, so driving them via a regular onScroll callback (rather than
35
35
  // a useAnimatedScrollHandler worklet, which requires an Animated-wrapped component to attach to at
36
- // the native level) works fine here — the header/footer animations just pick up the change on
36
+ // the native level) works fine here - the header/footer animations just pick up the change on
37
37
  // their next UI-thread frame. This trades a small amount of latency for a plain scrollable that
38
38
  // reliably honors the initial `contentOffset` prop on (re)mount, which Animated.FlatList does not.
39
39
  export function useScrollHandlerJS({ capturedGeneration, chipHidden, chipThreshold = 100, footerFixed, headerFixed, isHorizontal, jsListGeneration, remountTarget = null, scrollTo }: UseScrollHandlerJSOptions): ScrollHandlerJS {
40
40
  const { chromeHosted, chromeWritable, footerOffset, headerHeightShared, headerOffset, pullSearchHeightShared, scrollPosition, snapBackFooterShared, snapBackHeaderShared, stackHeightShared } = useContext(ScrollViewContext)
41
41
  const snapUpAccum = useRef(0)
42
42
  const fromBottomBounce = useRef(false)
43
- // On a remount, the very first native position after mount isn't reliable — Fabric can skip
43
+ // On a remount, the very first native position after mount isn't reliable - Fabric can skip
44
44
  // reapplying `contentOffset` when the target is numerically unchanged from before, and native
45
45
  // scroll settling can overshoot or undershoot it. Rather than trust either the declarative prop
46
- // or a single imperative scrollTo call, keep correcting (without hiding anything — content stays
46
+ // or a single imperative scrollTo call, keep correcting (without hiding anything - content stays
47
47
  // visible throughout) until an onScroll event actually confirms the intended resting position.
48
48
  const pendingRemountTarget = useRef(remountTarget)
49
49
  const remountAttempts = useRef(0)
@@ -127,7 +127,7 @@ export function useScrollHandlerJS({ capturedGeneration, chipHidden, chipThresho
127
127
 
128
128
  // Hosted: scroll tracks 1:1, so it can stop with the stack half hidden; it settles to fully shown
129
129
  // or fully hidden instead, since the bar is the app's primary navigation. `revealing` is whether
130
- // the 10pt scroll-up latch has already started animating the offset back to 0 — settling on the
130
+ // the 10pt scroll-up latch has already started animating the offset back to 0 - settling on the
131
131
  // in-flight value instead could pick "hidden" and reverse a deliberate reveal.
132
132
  const settle = useCallback(
133
133
  (revealing: boolean) => {
@@ -29,7 +29,7 @@ export function useScrollList({ footerFixed: footerFixedProp, headerFixed: heade
29
29
 
30
30
  const headerFixed = isHorizontal ? true : (headerFixedProp ?? contextHeaderFixed)
31
31
  // Hosted, the footer and the bar share ONE offset that the provider's registrar reveals when Footer
32
- // Lock turns on — and the registrar only sees the provider-level flag. A per-list override that
32
+ // Lock turns on - and the registrar only sees the provider-level flag. A per-list override that
33
33
  // turned the handlers off on its own would leave the stack stuck wherever it was hidden.
34
34
  const footerFixed = isHorizontal ? true : chromeHosted ? contextFooterFixed : (footerFixedProp ?? contextFooterFixed)
35
35
 
@@ -37,12 +37,12 @@ export function useScrollList({ footerFixed: footerFixedProp, headerFixed: heade
37
37
  pullSearchHeightShared.value = usesContentInset ? (pullSearchHeight ?? 0) : 0
38
38
  }, [pullSearchHeight, pullSearchHeightShared])
39
39
 
40
- // First render (headerHeight===null): flex:1, sitting naturally below the in-flow header —
40
+ // First render (headerHeight===null): flex:1, sitting naturally below the in-flow header -
41
41
  // same visual position content ends up in after the swap below (absoluteFill + paddingTop
42
42
  // matching the now-measured headerHeight), so no hiding is needed for this transition on its
43
43
  // own. hideUntilMeasured is only for SectionList: its key={headerHeight>0?1:0} forces a real
44
44
  // remount of the underlying native list on this exact transition, and that remount is what
45
- // needs hiding, not this container swap itself — ScrollView/FlatList/CustomList never remount
45
+ // needs hiding, not this container swap itself - ScrollView/FlatList/CustomList never remount
46
46
  // here, so unconditionally hiding this container regressed a real "flash of blank" on every
47
47
  // fresh mount for them (2026-08-03).
48
48
  const containerStyle = useMemo(() => (headerHeight === null ? [{ flex: 1, opacity: hideUntilMeasured ? 0 : 1 }, style] : [StyleSheet.absoluteFill, style]), [headerHeight, hideUntilMeasured, style])
@@ -53,7 +53,7 @@ export function useScrollList({ footerFixed: footerFixedProp, headerFixed: heade
53
53
  // ScrollViewFooter's own paddingBottom).
54
54
  //
55
55
  // When footerAboveKeyboard opts the (fixed) footer into floating above the keyboard instead of
56
- // sitting behind it, footerHeight ALREADY includes the current keyboardHeight — ScrollViewFooter
56
+ // sitting behind it, footerHeight ALREADY includes the current keyboardHeight - ScrollViewFooter
57
57
  // grows its own container by keyboardHeight to float (see its containerPaddingBottom), rather
58
58
  // than translating a fixed-height bar, so onLayout reports the combined height. Adding
59
59
  // keyboardHeight again here would double-count it. Otherwise (not floating) ScrollViewFooter pins
@@ -64,7 +64,7 @@ export function useScrollList({ footerFixed: footerFixedProp, headerFixed: heade
64
64
  // is actually taller right now.
65
65
  //
66
66
  // Hosted, tabBarHeight is the bar's footprint and it already contains the safe-area inset, so the
67
- // base reserve is only what pokes above the bar (chromeOverhang) — not insets.bottom a second
67
+ // base reserve is only what pokes above the bar (chromeOverhang) - not insets.bottom a second
68
68
  // time. Hosted also reserves the screen footer's own height whether or not it is fixed: unlike an
69
69
  // unhosted footer (whose position is a function of scroll, so scrolling to the end always hides
70
70
  // it), a hosted stack settles wherever the last gesture left it, so a revealed footer at the end
@@ -82,7 +82,7 @@ export function useScrollList({ footerFixed: footerFixedProp, headerFixed: heade
82
82
  return { bottom, top: headerHeight ?? 0 }
83
83
  }, [chromeHosted, chromeOverhang, footerAboveKeyboard, footerFixed, footerHeight, insets.bottom, tabBarHeight, headerHeight, keyboardAware, keyboardHeight])
84
84
  // Outside inset mode the same geometry is applied as content padding instead; contentInset is
85
- // zeroed (not omitted) so consumers' offset math — scroll-to-top targets, minHeight — stays
85
+ // zeroed (not omitted) so consumers' offset math - scroll-to-top targets, minHeight - stays
86
86
  // correct in the raw 0-based coordinate space those platforms actually scroll in.
87
87
  const contentInset = usesContentInset ? insetGeometry : ZERO_INSET
88
88
  const contentPadding = useMemo(() => (usesContentInset || isHorizontal ? null : { paddingBottom: insetGeometry.bottom, paddingTop: insetGeometry.top }), [insetGeometry, isHorizontal])
@@ -90,19 +90,19 @@ export function useScrollList({ footerFixed: footerFixedProp, headerFixed: heade
90
90
  // Use hideY offset only when this component mounted with headerHeight already known (mode switch).
91
91
  // On initial load, headerHeight is null at mount so we start at showY and let useScrollInit
92
92
  // call scrollTo after the phase transition. On mode switch, headerHeight is already set so we
93
- // can position the FlatList at hideY atomically via contentOffset — no scrollTo needed.
93
+ // can position the FlatList at hideY atomically via contentOffset - no scrollTo needed.
94
94
  const [startedWithHeader] = useState(headerHeight !== null)
95
95
  const insetContentOffset = useMemo(() => ({ x: 0, y: headerHeight !== null ? -headerHeight + (startedWithHeader && !isHorizontal && pullSearchHeight ? pullSearchHeight : 0) : 0 }), [headerHeight, isHorizontal, pullSearchHeight, startedWithHeader])
96
96
  const contentOffset = usesContentInset ? insetContentOffset : ZERO_OFFSET
97
97
 
98
98
  const chipHidden = useSharedValue(1)
99
99
  // chipHidden/snapBackHeaderShared/headerOffset/scrollPosition/pullSearchHeightShared/
100
- // headerHeightShared all need to be listed here too, not just read — see ScrollViewHeader's
100
+ // headerHeightShared all need to be listed here too, not just read - see ScrollViewHeader's
101
101
  // translateStyle for why (web-only reanimated reactivity gap: a SharedValue's mutations don't
102
102
  // retrigger useAnimatedStyle unless the value itself is in this array). headerHeightShared is
103
103
  // used here rather than the plain `headerHeight` context value specifically because the latter
104
104
  // changes identity (null -> a real number) shortly after mount, which forces Reanimated to
105
- // recreate this worklet — a crash on react-native-worklets 0.10.1, not just a perf concern. See
105
+ // recreate this worklet - a crash on react-native-worklets 0.10.1, not just a perf concern. See
106
106
  // ScrollViewHeader's translateStyle/blurStyle/progressStyle for the full explanation.
107
107
  const chipStyle = useAnimatedStyle(() => {
108
108
  const pointerEvents = chipHidden.value ? ('none' as const) : ('box-none' as const)
@@ -26,7 +26,7 @@ export function useStickyHeaders(headerFixed: boolean, enabled: boolean): Sticky
26
26
  contentIndexShared.value = activeIndex
27
27
  }, [activeIndex, contentIndexShared])
28
28
 
29
- // JS-side source of truth — sectionYPositions is a snapshot pushed to the worklet.
29
+ // JS-side source of truth - sectionYPositions is a snapshot pushed to the worklet.
30
30
  // Reading sectionYPositions.value from JS returns the UI-thread's stale copy, so
31
31
  // rapid async measure callbacks would overwrite each other; refs avoid that.
32
32
  const positionsCache = useRef<number[]>([])
@@ -65,7 +65,7 @@ export function useStickyHeaders(headerFixed: boolean, enabled: boolean): Sticky
65
65
  // Same web-only reanimated gap as useAnimatedStyle elsewhere in this file: this "prepare"
66
66
  // function's own SharedValue reads (sectionYPositions, headerHeightShared, scrollPosition,
67
67
  // snapBackHeaderShared, headerOffset, pullSearchHeightShared) need to be listed explicitly too
68
- // — confirmed empirically that without them, useAnimatedReaction re-evaluates once on mount and
68
+ // - confirmed empirically that without them, useAnimatedReaction re-evaluates once on mount and
69
69
  // then never again on web, permanently freezing activeIndex/the active sticky section.
70
70
  [enabled, headerFixed, insetsTop, sectionYPositions, headerHeightShared, scrollPosition, snapBackHeaderShared, headerOffset, pullSearchHeightShared]
71
71
  )
@@ -74,7 +74,7 @@ export function useStickyHeaders(headerFixed: boolean, enabled: boolean): Sticky
74
74
  // header as it slides above the sticky line during a push transition.
75
75
  // Every SharedValue read below (headerHeightShared, scrollPosition, snapBackHeaderShared,
76
76
  // headerOffset, pullSearchHeightShared, and for overlayStyle also contentIndexShared/
77
- // sectionYPositions/sectionHeights) has to be listed in the array too — see
77
+ // sectionYPositions/sectionHeights) has to be listed in the array too - see
78
78
  // ScrollViewHeader's translateStyle for why: on web, reanimated doesn't treat mutations to a
79
79
  // SharedValue as a reactive trigger for useAnimatedStyle unless it's in this array.
80
80
  const clipStyle = useAnimatedStyle(() => {
@@ -2,7 +2,7 @@ import { createSettingsSlice } from '@rific/core'
2
2
 
3
3
  import { defaultScrollViewSettings, type ScrollViewSettings } from '../ScrollViewSettingsContext'
4
4
 
5
- // scrollView has no per-field setters, just a full-replace initialize — matches the previous
5
+ // scrollView has no per-field setters, just a full-replace initialize - matches the previous
6
6
  // hand-rolled slice exactly (action type 'scrollView/initialize', full-state-replace behavior).
7
7
  const scrollViewSlice = createSettingsSlice<ScrollViewSettings>('scrollView', {
8
8
  initialState: defaultScrollViewSettings,
@@ -23,6 +23,6 @@ const useKeyboardInsetNative = () => {
23
23
 
24
24
  const useKeyboardInsetWeb = () => 0
25
25
 
26
- // react-native-keyboard-controller has no web implementation — its handler hook must never be
26
+ // react-native-keyboard-controller has no web implementation - its handler hook must never be
27
27
  // called there. Platform is constant for the life of the bundle, so this select is hook-safe.
28
28
  export const useKeyboardInset = Platform.OS === 'web' ? useKeyboardInsetWeb : useKeyboardInsetNative
@@ -12,7 +12,7 @@ const REMOUNT_PRIME_FRAMES = 40
12
12
  const SNAP_VERIFY_TOLERANCE = 1
13
13
  const SNAP_VERIFY_MAX_ATTEMPTS = 5
14
14
  // Long enough to comfortably outlast RN's default animated scrollTo duration (iOS's native
15
- // setContentOffset:animated: for a typical pull-search distance settles well within this) — see
15
+ // setContentOffset:animated: for a typical pull-search distance settles well within this) - see
16
16
  // the comment on scheduleSnapVerification for why a native onMomentumScrollEnd event can't be used
17
17
  // as this signal instead.
18
18
  const SNAP_VERIFY_INITIAL_DELAY_MS = 400
@@ -51,7 +51,7 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
51
51
  // switch). Only enter 'measuring' on the very first mount when headerHeight hasn't been set yet.
52
52
  const [phase, setPhase] = useState<'measuring' | 'ready'>(hasPullSearch && !headerHeight ? 'measuring' : 'ready')
53
53
  const [refreshing, setRefreshing] = useState(false)
54
- // A plain-ref mirror of `refreshing`, read from inside the deferred timeout below — that
54
+ // A plain-ref mirror of `refreshing`, read from inside the deferred timeout below - that
55
55
  // callback can fire well after the render that scheduled it, so it needs the *current* value,
56
56
  // not whatever `refreshing` closed over at schedule time.
57
57
  const refreshingRef = useRef(refreshing)
@@ -60,7 +60,7 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
60
60
  }, [refreshing])
61
61
  const dragStartY = useRef<number | null>(null)
62
62
  // Which pull-search resting position (showY or hideY) a drag-release most recently committed
63
- // to, so a late fallback in handleMomentumScrollEnd knows not to fight it — see
63
+ // to, so a late fallback in handleMomentumScrollEnd knows not to fight it - see
64
64
  // scheduleSnapVerification below for how this actually gets enforced.
65
65
  const pendingSnapTarget = useRef<number | null>(null)
66
66
  const snapVerifyTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)
@@ -69,9 +69,9 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
69
69
  // Verifies a pull-search drag-release (handleScrollEndDrag) actually landed on `target`, and
70
70
  // keeps correcting (non-animated, so it lands immediately) until it does. This is deliberately
71
71
  // time-based rather than triggered by onMomentumScrollEnd: a slow/controlled release with near-
72
- // zero velocity fires onMomentumScrollEnd within a couple of milliseconds of the drag ending —
72
+ // zero velocity fires onMomentumScrollEnd within a couple of milliseconds of the drag ending -
73
73
  // long before the *animated* scrollTo call issued moments earlier has had any chance to actually
74
- // move — so reacting to that event by immediately snapping (non-animated) to the target replaced
74
+ // move - so reacting to that event by immediately snapping (non-animated) to the target replaced
75
75
  // the intended smooth spring-back animation with a jarring instant jump on every normal pull,
76
76
  // not just the pathological case this was built for. Waiting out a fixed delay long enough for a
77
77
  // normal animated scrollTo to have finished, then checking the *actual* position via
@@ -111,7 +111,7 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
111
111
  []
112
112
  )
113
113
 
114
- // Remount sync: this instance mounted with headerHeight already known — a key-forced remount
114
+ // Remount sync: this instance mounted with headerHeight already known - a key-forced remount
115
115
  // (e.g. numColumns toggling between a list and a grid), not a true first mount. The native
116
116
  // scroll view's actual position right after such a remount isn't reliable (Fabric's ScrollView
117
117
  // recycling and Reanimated's Animated.FlatList can both leave it at a stale or zeroed offset
@@ -159,7 +159,7 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
159
159
  }, REMOUNT_SYNC_FALLBACK_MS)
160
160
  return () => clearTimeout(fallback)
161
161
  // Intentionally runs once per mount only, using whatever remountTarget was captured at
162
- // construction — not a dep that should retrigger this effect.
162
+ // construction - not a dep that should retrigger this effect.
163
163
  // eslint-disable-next-line react-hooks/exhaustive-deps
164
164
  }, [])
165
165
 
@@ -170,10 +170,10 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
170
170
  setPhase('ready')
171
171
  }, [phase, headerHeight, pullSearchHeight, scrollPosition])
172
172
 
173
- // Effect A applies ONLY on an actual remount (remountTarget !== null — this instance mounted with
173
+ // Effect A applies ONLY on an actual remount (remountTarget !== null - this instance mounted with
174
174
  // headerHeight already known, e.g. a key-forced remount elsewhere in the tree). It syncs
175
175
  // scrollPosition synchronously before first paint and issues a short bounded chain of imperative
176
- // scrollTo calls, alternating between two offsets far enough apart (30pt — comfortably more than
176
+ // scrollTo calls, alternating between two offsets far enough apart (30pt - comfortably more than
177
177
  // any plausible rounding/measurement noise) to guarantee at least one call can't be mistaken for a
178
178
  // no-op. The declarative `contentOffset` prop alone isn't enough on a remount: when the target is
179
179
  // numerically identical to what it was before (headerHeight/pullSearchHeight don't change across a
@@ -181,7 +181,7 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
181
181
  // be left at whatever position it actually drifted to. Worse, iOS's imperative scrollToOffset
182
182
  // short-circuits with a CGPointEqualToPoint check against the native view's *actual* current
183
183
  // contentOffset before it will fire the native scroll-finished callback that RN turns into an
184
- // onScroll event — so a single differing call can still go unnoticed if it happens to race content
184
+ // onScroll event - so a single differing call can still go unnoticed if it happens to race content
185
185
  // that's still growing. Alternating across the *entire* correction window, not just the first
186
186
  // couple of frames, forces a real scroll-finished callback so drift becomes observable and the
187
187
  // onScroll-driven retry loop in useScrollHandlerJS can correct it.
@@ -189,7 +189,7 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
189
189
  // On a true first mount (remountTarget === null) this effect does nothing beyond a single
190
190
  // insurance call against the destination view silently ignoring the very first scroll command
191
191
  // because it isn't fully installed in the window hierarchy yet (e.g. mid native-stack push
192
- // transition) — a fresh, non-recycled native view reliably honors the declarative contentOffset
192
+ // transition) - a fresh, non-recycled native view reliably honors the declarative contentOffset
193
193
  // prop otherwise. Repeating or oscillating that call here fights Effect B below, whose entire job
194
194
  // on a true first mount with pull-search is to push the position on from here to hideY: if this
195
195
  // effect keeps re-asserting its own target for tens of frames, it can win the race and leave the
@@ -277,7 +277,7 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
277
277
  if (pullSearchHeight && headerHeight && !refreshing && pendingSnapTarget.current === null) {
278
278
  // No drag committed to a target above (e.g. a fast fling whose whole reveal/overscroll
279
279
  // motion happened during momentum, never during the drag itself, so handleScrollEndDrag
280
- // never saw a y near the pull-search zone) — fall back to auto-closing if we've ended up
280
+ // never saw a y near the pull-search zone) - fall back to auto-closing if we've ended up
281
281
  // revealed without ever meaning to.
282
282
  const y = e.nativeEvent.contentOffset.y
283
283
  const hideY = -headerHeight + pullSearchHeight