@rific/scroll-view 0.9.2 → 1.1.0

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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Blur-chrome scroll system for React Native. Drop-in replacements for `ScrollView`, `FlatList`, and `SectionList` with floating headers and footers, pull-to-search, keyboard awareness, horizontal paging, and frosted-glass chrome.
4
4
 
5
+ **[Live demo](https://rific.jaydeaton.com/demos/scroll-view)**
6
+
5
7
  ## Installation
6
8
 
7
9
  ```sh
@@ -63,6 +65,27 @@ App-wide defaults for scroll settings. Wrap your root layout to set global defau
63
65
 
64
66
  ---
65
67
 
68
+ ### `ScrollViewConfigProvider`
69
+
70
+ App-wide component config, set once at startup. Today it holds one thing: the component `ScrollViewHeader` renders for a `backAction` callback. Pass `AppbarBackAction` from [`@rific/feedback-press`](https://www.npmjs.com/package/@rific/feedback-press) and every header's back button gets the same haptics and sound as the rest of your buttons, without touching each screen. This package doesn't depend on feedback-press; you hand it the component.
71
+
72
+ | Prop | Type | Default | Description |
73
+ |------|------|---------|-------------|
74
+ | `BackAction` | `ComponentType<ScrollViewBackActionProps>` | Paper's `Appbar.BackAction` | Rendered for a `backAction` callback. Receives `onPress`, `size`, `style` and `accessibilityLabel` (from `backActionAccessibilityLabel`), the same props the default gets |
75
+ | `children` | `ReactNode` | - | - |
76
+
77
+ ```tsx
78
+ import { AppbarBackAction } from '@rific/feedback-press'
79
+ import { ScrollViewConfigProvider } from '@rific/scroll-view'
80
+
81
+ // Inside FeedbackPressProvider, since AppbarBackAction reads it
82
+ <ScrollViewConfigProvider BackAction={AppbarBackAction}>{children}</ScrollViewConfigProvider>
83
+ ```
84
+
85
+ Or call `configureScrollView({ BackAction: AppbarBackAction })` once at module load; `getScrollViewConfig()` reads it back. Like the other `@rific` module configs (`DrawerProvider`, `ResizableInputProvider`), this is plain startup config, not React state: the provider sets it during its own render, before anything under it renders, and changing it later doesn't re-render headers already on screen. A `backAction` element on one header still replaces the configured component there.
86
+
87
+ ---
88
+
66
89
  ### `ScrollViewProvider`
67
90
 
68
91
  Screen-level provider that owns header/footer state and scroll position. Must wrap `ScrollViewHeader`, `ScrollViewFooter`, and the scroll component.
@@ -108,7 +131,7 @@ Floating header with blur backdrop, title, back action, trailing action, and bui
108
131
  | `caption` | `string` | - | Subtitle below title |
109
132
  | `centerContent` | `ReactNode` | - | Replaces title/caption with custom content |
110
133
  | `children` | `ReactNode` | - | Left-side content (icon buttons, etc.) |
111
- | `backAction` | `() => void` | - | Renders a back button and calls this on press |
134
+ | `backAction` | `(() => void) \| ReactNode` | - | A callback renders a back button (the [configured `BackAction`](#scrollviewconfigprovider), Paper's `Appbar.BackAction` by default) and calls it on press. An element is rendered in that spot instead |
112
135
  | `backActionFixed` | `boolean` | settings | Keep back button visible as header scrolls away |
113
136
  | `trailingAction` | `ReactNode` | - | Right-side floating action |
114
137
  | `trailingActionFixed` | `boolean` | `true` | Keep trailing action visible as header scrolls away |
package/dist/index.d.mts CHANGED
@@ -166,6 +166,23 @@ type ScrollViewChromeProviderProps = {
166
166
  };
167
167
  declare const ScrollViewChromeProvider: ({ children, footprint, overhang, snapBack, useIsActive }: ScrollViewChromeProviderProps) => react.JSX.Element;
168
168
 
169
+ type ScrollViewBackActionProps = {
170
+ accessibilityLabel?: string;
171
+ onPress: () => void;
172
+ size: number;
173
+ style: ViewStyle;
174
+ };
175
+ type ScrollViewConfig = {
176
+ /** Component ScrollViewHeader renders when `backAction` is a plain callback. Pass `AppbarBackAction` from `@rific/feedback-press` to give every header's back button its haptics and sound; omit to keep react-native-paper's `Appbar.BackAction`. A `backAction` element on one header still replaces it there. */
177
+ BackAction?: ComponentType<ScrollViewBackActionProps>;
178
+ };
179
+ declare const configureScrollView: (next: Partial<ScrollViewConfig>) => void;
180
+ declare const getScrollViewConfig: () => ScrollViewConfig;
181
+ declare const ScrollViewConfigProvider: (props: _rific_core.ModuleConfigProviderProps<ScrollViewConfig>) => React.JSX.Element;
182
+ type ScrollViewConfigProviderProps = ScrollViewConfig & {
183
+ children: ReactNode;
184
+ };
185
+
169
186
  type ScrollViewContextType = {
170
187
  blur: boolean;
171
188
  chromeHosted: boolean;
@@ -285,4 +302,4 @@ declare const useScrollViewChromeReveal: () => () => void;
285
302
 
286
303
  declare const useScrollViewSettings: () => _rific_core.SettingsContextValue<ScrollViewSettings>;
287
304
 
288
- export { type ChipProps, CustomList, type CustomListProps, FlatList, type FlatListProps, PullSearch, type PullSearchHandle, type PullSearchProps, ScrollView, ScrollViewChromeProvider, type ScrollViewChromeProviderProps, ScrollViewContext, type ScrollViewContextType, ScrollViewFooter, type ScrollViewFooterProps, ScrollViewHeader, type ScrollViewHeaderProps, type ScrollViewProps, ScrollViewProvider, type ScrollViewProviderProps, type ScrollViewSettings, ScrollViewSettingsContext, type ScrollViewSettingsContextType, ScrollViewSettingsProvider, type ScrollViewSettingsProviderProps, SectionList, type SectionListProps, defaultScrollViewSettings, scrollViewActions, scrollViewReducer, useKeyboardInset, useScrollView, useScrollViewChromePin, useScrollViewChromeReveal, useScrollViewChromeStyle, useScrollViewSettings };
305
+ export { type ChipProps, CustomList, type CustomListProps, FlatList, type FlatListProps, PullSearch, type PullSearchHandle, type PullSearchProps, ScrollView, type ScrollViewBackActionProps, ScrollViewChromeProvider, type ScrollViewChromeProviderProps, type ScrollViewConfig, ScrollViewConfigProvider, type ScrollViewConfigProviderProps, ScrollViewContext, type ScrollViewContextType, ScrollViewFooter, type ScrollViewFooterProps, ScrollViewHeader, type ScrollViewHeaderProps, type ScrollViewProps, ScrollViewProvider, type ScrollViewProviderProps, type ScrollViewSettings, ScrollViewSettingsContext, type ScrollViewSettingsContextType, ScrollViewSettingsProvider, type ScrollViewSettingsProviderProps, SectionList, type SectionListProps, configureScrollView, defaultScrollViewSettings, getScrollViewConfig, scrollViewActions, scrollViewReducer, useKeyboardInset, useScrollView, useScrollViewChromePin, useScrollViewChromeReveal, useScrollViewChromeStyle, useScrollViewSettings };
package/dist/index.d.ts CHANGED
@@ -166,6 +166,23 @@ type ScrollViewChromeProviderProps = {
166
166
  };
167
167
  declare const ScrollViewChromeProvider: ({ children, footprint, overhang, snapBack, useIsActive }: ScrollViewChromeProviderProps) => react.JSX.Element;
168
168
 
169
+ type ScrollViewBackActionProps = {
170
+ accessibilityLabel?: string;
171
+ onPress: () => void;
172
+ size: number;
173
+ style: ViewStyle;
174
+ };
175
+ type ScrollViewConfig = {
176
+ /** Component ScrollViewHeader renders when `backAction` is a plain callback. Pass `AppbarBackAction` from `@rific/feedback-press` to give every header's back button its haptics and sound; omit to keep react-native-paper's `Appbar.BackAction`. A `backAction` element on one header still replaces it there. */
177
+ BackAction?: ComponentType<ScrollViewBackActionProps>;
178
+ };
179
+ declare const configureScrollView: (next: Partial<ScrollViewConfig>) => void;
180
+ declare const getScrollViewConfig: () => ScrollViewConfig;
181
+ declare const ScrollViewConfigProvider: (props: _rific_core.ModuleConfigProviderProps<ScrollViewConfig>) => React.JSX.Element;
182
+ type ScrollViewConfigProviderProps = ScrollViewConfig & {
183
+ children: ReactNode;
184
+ };
185
+
169
186
  type ScrollViewContextType = {
170
187
  blur: boolean;
171
188
  chromeHosted: boolean;
@@ -285,4 +302,4 @@ declare const useScrollViewChromeReveal: () => () => void;
285
302
 
286
303
  declare const useScrollViewSettings: () => _rific_core.SettingsContextValue<ScrollViewSettings>;
287
304
 
288
- export { type ChipProps, CustomList, type CustomListProps, FlatList, type FlatListProps, PullSearch, type PullSearchHandle, type PullSearchProps, ScrollView, ScrollViewChromeProvider, type ScrollViewChromeProviderProps, ScrollViewContext, type ScrollViewContextType, ScrollViewFooter, type ScrollViewFooterProps, ScrollViewHeader, type ScrollViewHeaderProps, type ScrollViewProps, ScrollViewProvider, type ScrollViewProviderProps, type ScrollViewSettings, ScrollViewSettingsContext, type ScrollViewSettingsContextType, ScrollViewSettingsProvider, type ScrollViewSettingsProviderProps, SectionList, type SectionListProps, defaultScrollViewSettings, scrollViewActions, scrollViewReducer, useKeyboardInset, useScrollView, useScrollViewChromePin, useScrollViewChromeReveal, useScrollViewChromeStyle, useScrollViewSettings };
305
+ export { type ChipProps, CustomList, type CustomListProps, FlatList, type FlatListProps, PullSearch, type PullSearchHandle, type PullSearchProps, ScrollView, type ScrollViewBackActionProps, ScrollViewChromeProvider, type ScrollViewChromeProviderProps, type ScrollViewConfig, ScrollViewConfigProvider, type ScrollViewConfigProviderProps, ScrollViewContext, type ScrollViewContextType, ScrollViewFooter, type ScrollViewFooterProps, ScrollViewHeader, type ScrollViewHeaderProps, type ScrollViewProps, ScrollViewProvider, type ScrollViewProviderProps, type ScrollViewSettings, ScrollViewSettingsContext, type ScrollViewSettingsContextType, ScrollViewSettingsProvider, type ScrollViewSettingsProviderProps, SectionList, type SectionListProps, configureScrollView, defaultScrollViewSettings, getScrollViewConfig, scrollViewActions, scrollViewReducer, useKeyboardInset, useScrollView, useScrollViewChromePin, useScrollViewChromeReveal, useScrollViewChromeStyle, useScrollViewSettings };
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  PullSearch: () => PullSearch,
36
36
  ScrollView: () => ScrollView,
37
37
  ScrollViewChromeProvider: () => ScrollViewChromeProvider,
38
+ ScrollViewConfigProvider: () => ScrollViewConfigProvider,
38
39
  ScrollViewContext: () => ScrollViewContext,
39
40
  ScrollViewFooter: () => ScrollViewFooter,
40
41
  ScrollViewHeader: () => ScrollViewHeader,
@@ -42,7 +43,9 @@ __export(index_exports, {
42
43
  ScrollViewSettingsContext: () => ScrollViewSettingsContext,
43
44
  ScrollViewSettingsProvider: () => ScrollViewSettingsProvider,
44
45
  SectionList: () => SectionList,
46
+ configureScrollView: () => configureScrollView,
45
47
  defaultScrollViewSettings: () => defaultScrollViewSettings,
48
+ getScrollViewConfig: () => getScrollViewConfig,
46
49
  scrollViewActions: () => scrollViewActions,
47
50
  scrollViewReducer: () => scrollViewReducer,
48
51
  useKeyboardInset: () => useKeyboardInset,
@@ -87,6 +90,7 @@ var styles = import_react_native2.StyleSheet.create({
87
90
  });
88
91
 
89
92
  // src/internal/useScrollHandler.ts
93
+ var import_core = require("@rific/core");
90
94
  var import_react3 = require("react");
91
95
  var import_react_native_reanimated3 = require("react-native-reanimated");
92
96
 
@@ -335,6 +339,8 @@ function useScrollHandler({ capturedGeneration, chipHidden, chipThreshold = 100,
335
339
  const { chromeHosted, chromeWritable, footerOffset, headerHeightShared, headerOffset, pullSearchHeightShared, scrollPosition, snapBackFooterShared, snapBackHeaderShared, stackHeightShared } = (0, import_react3.useContext)(ScrollViewContext);
336
340
  const snapUpAccum = (0, import_react_native_reanimated3.useSharedValue)(0);
337
341
  const fromBottomBounce = (0, import_react_native_reanimated3.useSharedValue)(false);
342
+ const remountSyncedKey = (0, import_core.useWorkletCallback)((y) => onRemountSynced?.(y));
343
+ const remountSyncRetryKey = (0, import_core.useWorkletCallback)((y) => onRemountSyncRetry?.(y));
338
344
  return (0, import_react_native_reanimated3.useAnimatedScrollHandler)(
339
345
  {
340
346
  onScroll: ({ contentOffset: { x, y }, contentSize: { height: contentHeight }, layoutMeasurement: { height: layoutHeight } }) => {
@@ -343,9 +349,9 @@ function useScrollHandler({ capturedGeneration, chipHidden, chipThreshold = 100,
343
349
  const target = remountSyncTarget.value;
344
350
  if (Math.abs(y - target) < REMOUNT_SYNC_TOLERANCE) {
345
351
  remountSyncTarget.value = null;
346
- if (onRemountSynced) (0, import_react_native_reanimated3.runOnJS)(onRemountSynced)(y);
352
+ if (onRemountSynced) (0, import_react_native_reanimated3.runOnJS)(import_core.callWorkletCallback)(remountSyncedKey, y);
347
353
  } else if (onRemountSyncRetry) {
348
- (0, import_react_native_reanimated3.runOnJS)(onRemountSyncRetry)(y);
354
+ (0, import_react_native_reanimated3.runOnJS)(import_core.callWorkletCallback)(remountSyncRetryKey, y);
349
355
  }
350
356
  return;
351
357
  }
@@ -429,21 +435,23 @@ var import_react_native_reanimated5 = require("react-native-reanimated");
429
435
  var import_react_native_safe_area_context = require("react-native-safe-area-context");
430
436
 
431
437
  // src/useKeyboardInset.ts
438
+ var import_core2 = require("@rific/core");
432
439
  var import_react4 = require("react");
433
440
  var import_react_native4 = require("react-native");
434
441
  var import_react_native_keyboard_controller = require("react-native-keyboard-controller");
435
442
  var import_react_native_reanimated4 = require("react-native-reanimated");
436
443
  var useKeyboardInsetNative = () => {
437
444
  const [keyboardHeight, setKeyboardHeight] = (0, import_react4.useState)(0);
445
+ const keyboardHeightKey = (0, import_core2.useWorkletCallback)(setKeyboardHeight);
438
446
  (0, import_react_native_keyboard_controller.useKeyboardHandler)(
439
447
  {
440
448
  onMove: (e) => {
441
449
  "worklet";
442
- (0, import_react_native_reanimated4.runOnJS)(setKeyboardHeight)(e.height);
450
+ (0, import_react_native_reanimated4.runOnJS)(import_core2.callWorkletCallback)(keyboardHeightKey, e.height);
443
451
  },
444
452
  onEnd: (e) => {
445
453
  "worklet";
446
- (0, import_react_native_reanimated4.runOnJS)(setKeyboardHeight)(e.height);
454
+ (0, import_react_native_reanimated4.runOnJS)(import_core2.callWorkletCallback)(keyboardHeightKey, e.height);
447
455
  }
448
456
  },
449
457
  []
@@ -926,6 +934,7 @@ var styles4 = { measureContainer: { left: 0, opacity: 0, position: "absolute", r
926
934
  var FlatList = (0, import_react9.memo)(FlatListInner);
927
935
 
928
936
  // src/PullSearch.tsx
937
+ var import_core3 = require("@rific/core");
929
938
  var import_react10 = require("react");
930
939
  var import_react_native9 = require("react-native");
931
940
  var import_react_native_paper3 = require("react-native-paper");
@@ -948,13 +957,14 @@ var PullSearch = (0, import_react10.forwardRef)(function PullSearch2({ debounce,
948
957
  focus: () => inputRef.current?.focus()
949
958
  }));
950
959
  const blurInput = (0, import_react10.useCallback)(() => inputRef.current?.blur(), []);
960
+ const blurKey = (0, import_core3.useWorkletCallback)(blurInput);
951
961
  (0, import_react_native_reanimated9.useAnimatedReaction)(
952
962
  () => {
953
963
  if (barHeight === 0) return false;
954
964
  return scrollPosition.value >= -(headerHeight ?? 0) + barHeight;
955
965
  },
956
966
  (isHidden, wasHidden) => {
957
- if (isHidden && !wasHidden) (0, import_react_native_reanimated9.runOnJS)(blurInput)();
967
+ if (isHidden && !wasHidden) (0, import_react_native_reanimated9.runOnJS)(import_core3.callWorkletCallback)(blurKey);
958
968
  },
959
969
  // scrollPosition has to be listed too, not just read - see useStickyHeaders' useAnimatedReaction
960
970
  // for why: on web, without reanimated's Babel plugin active, an unlisted SharedValue mutation
@@ -1000,23 +1010,23 @@ var styles5 = import_react_native9.StyleSheet.create({
1000
1010
  });
1001
1011
 
1002
1012
  // src/redux/scrollViewSlice.ts
1003
- var import_core2 = require("@rific/core");
1013
+ var import_core5 = require("@rific/core");
1004
1014
 
1005
1015
  // src/ScrollViewSettingsContext.ts
1006
- var import_core = require("@rific/core");
1016
+ var import_core4 = require("@rific/core");
1007
1017
  var defaultScrollViewSettings = {
1008
1018
  backActionFixed: true,
1009
1019
  footerFixed: false,
1010
1020
  headerFixed: false,
1011
1021
  snapBack: false
1012
1022
  };
1013
- var settingsContext = (0, import_core.createSettingsContext)(defaultScrollViewSettings);
1023
+ var settingsContext = (0, import_core4.createSettingsContext)(defaultScrollViewSettings);
1014
1024
  var ScrollViewSettingsContext = settingsContext.Context;
1015
1025
  var scrollViewSettingsProvider = settingsContext.Provider;
1016
1026
  var scrollViewUseSettings = settingsContext.useSettings;
1017
1027
 
1018
1028
  // src/redux/scrollViewSlice.ts
1019
- var scrollViewSlice = (0, import_core2.createSettingsSlice)("scrollView", {
1029
+ var scrollViewSlice = (0, import_core5.createSettingsSlice)("scrollView", {
1020
1030
  initialState: defaultScrollViewSettings,
1021
1031
  fieldSetters: []
1022
1032
  });
@@ -1131,6 +1141,13 @@ var ScrollViewChromeProvider = ({ children, footprint, overhang = 0, snapBack =
1131
1141
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ScrollViewChromeContext.Provider, { value: chrome, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ScrollViewChromeHostContext.Provider, { value: host, children }) });
1132
1142
  };
1133
1143
 
1144
+ // src/ScrollViewConfig.tsx
1145
+ var import_core6 = require("@rific/core");
1146
+ var scrollViewConfig = (0, import_core6.createModuleConfig)();
1147
+ var configureScrollView = scrollViewConfig.configure;
1148
+ var getScrollViewConfig = scrollViewConfig.getConfig;
1149
+ var ScrollViewConfigProvider = scrollViewConfig.Provider;
1150
+
1134
1151
  // src/ScrollViewFooter.tsx
1135
1152
  var import_auto_paper3 = require("@rific/auto-paper");
1136
1153
  var import_react14 = require("react");
@@ -1253,6 +1270,8 @@ var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionAc
1253
1270
  }, [headerFixed, headerHeightShared, top, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1254
1271
  const actionTop = top + actionMargin;
1255
1272
  const buttonStyle = { borderRadius: actionSize, height: actionSize, margin: 0, width: actionSize };
1273
+ const BackAction = getScrollViewConfig().BackAction ?? import_react_native_paper4.Appbar.BackAction;
1274
+ const backActionElement = typeof backAction === "function" ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle }) : backAction;
1256
1275
  const actionShadow = { borderRadius: actionSize / 2, elevation: 2, shadowColor: "#000", shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.15, shadowRadius: 2 };
1257
1276
  const leadingStyle = { alignItems: "center", height: actionSize, justifyContent: "center", left: actionMargin, position: "absolute", top: actionTop, width: actionSize, zIndex: 3, ...actionShadow };
1258
1277
  const trailingStyle = { alignItems: "center", height: actionSize, justifyContent: "center", position: "absolute", right: actionMargin, top: actionTop, width: actionSize, zIndex: 3, ...actionShadow };
@@ -1272,10 +1291,10 @@ var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionAc
1272
1291
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native_reanimated13.default.View, { pointerEvents: "none", style: [styles7.progress, progressStyle], children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native_paper4.ProgressBar, { indeterminate: progress === null, visible: progressing, progress: progress ?? void 0 }) }),
1273
1292
  backAction && (effectiveBackActionFixed ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native_reanimated13.default.View, { style: leadingStyle, children: [
1274
1293
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ActionBg, { blur, style: actionStyle }),
1275
- typeof backAction === "function" ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native_paper4.Appbar.BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle }) : backAction
1294
+ backActionElement
1276
1295
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native_reanimated13.default.View, { style: [leadingStyle, translateStyle], children: [
1277
1296
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ActionBg, { blur, style: actionStyle }),
1278
- typeof backAction === "function" ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native_paper4.Appbar.BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle }) : backAction
1297
+ backActionElement
1279
1298
  ] })),
1280
1299
  trailingAction && (trailingActionFixed ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native12.View, { style: trailingStyle, children: [
1281
1300
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ActionBg, { blur, style: actionStyle }),
@@ -1441,6 +1460,7 @@ var import_react_native_gesture_handler4 = require("react-native-gesture-handler
1441
1460
  var import_react_native_reanimated16 = __toESM(require("react-native-reanimated"));
1442
1461
 
1443
1462
  // src/internal/useStickyHeaders.ts
1463
+ var import_core7 = require("@rific/core");
1444
1464
  var import_react18 = require("react");
1445
1465
  var import_react_native_reanimated15 = require("react-native-reanimated");
1446
1466
  var import_react_native_safe_area_context5 = require("react-native-safe-area-context");
@@ -1450,6 +1470,7 @@ function useStickyHeaders(headerFixed, enabled) {
1450
1470
  const sectionYPositions = (0, import_react_native_reanimated15.useSharedValue)([]);
1451
1471
  const sectionHeights = (0, import_react_native_reanimated15.useSharedValue)([]);
1452
1472
  const [activeIndex, setActiveIndex] = (0, import_react18.useState)(-1);
1473
+ const activeIndexKey = (0, import_core7.useWorkletCallback)(setActiveIndex);
1453
1474
  const contentIndexShared = (0, import_react_native_reanimated15.useSharedValue)(-1);
1454
1475
  (0, import_react18.useLayoutEffect)(() => {
1455
1476
  contentIndexShared.value = activeIndex;
@@ -1481,7 +1502,7 @@ function useStickyHeaders(headerFixed, enabled) {
1481
1502
  return active;
1482
1503
  },
1483
1504
  (curr, prev) => {
1484
- if (curr !== prev) (0, import_react_native_reanimated15.runOnJS)(setActiveIndex)(curr);
1505
+ if (curr !== prev) (0, import_react_native_reanimated15.runOnJS)(import_core7.callWorkletCallback)(activeIndexKey, curr);
1485
1506
  },
1486
1507
  // Same web-only reanimated gap as useAnimatedStyle elsewhere in this file: this "prepare"
1487
1508
  // function's own SharedValue reads (sectionYPositions, headerHeightShared, scrollPosition,
@@ -1743,6 +1764,7 @@ var useScrollViewSettings = scrollViewUseSettings;
1743
1764
  PullSearch,
1744
1765
  ScrollView,
1745
1766
  ScrollViewChromeProvider,
1767
+ ScrollViewConfigProvider,
1746
1768
  ScrollViewContext,
1747
1769
  ScrollViewFooter,
1748
1770
  ScrollViewHeader,
@@ -1750,7 +1772,9 @@ var useScrollViewSettings = scrollViewUseSettings;
1750
1772
  ScrollViewSettingsContext,
1751
1773
  ScrollViewSettingsProvider,
1752
1774
  SectionList,
1775
+ configureScrollView,
1753
1776
  defaultScrollViewSettings,
1777
+ getScrollViewConfig,
1754
1778
  scrollViewActions,
1755
1779
  scrollViewReducer,
1756
1780
  useKeyboardInset,
package/dist/index.mjs CHANGED
@@ -31,6 +31,7 @@ var styles = StyleSheet.create({
31
31
  });
32
32
 
33
33
  // src/internal/useScrollHandler.ts
34
+ import { callWorkletCallback, useWorkletCallback } from "@rific/core";
34
35
  import { useContext as useContext2 } from "react";
35
36
  import { runOnJS, useAnimatedScrollHandler, useSharedValue as useSharedValue2, withTiming } from "react-native-reanimated";
36
37
 
@@ -279,6 +280,8 @@ function useScrollHandler({ capturedGeneration, chipHidden, chipThreshold = 100,
279
280
  const { chromeHosted, chromeWritable, footerOffset, headerHeightShared, headerOffset, pullSearchHeightShared, scrollPosition, snapBackFooterShared, snapBackHeaderShared, stackHeightShared } = useContext2(ScrollViewContext);
280
281
  const snapUpAccum = useSharedValue2(0);
281
282
  const fromBottomBounce = useSharedValue2(false);
283
+ const remountSyncedKey = useWorkletCallback((y) => onRemountSynced?.(y));
284
+ const remountSyncRetryKey = useWorkletCallback((y) => onRemountSyncRetry?.(y));
282
285
  return useAnimatedScrollHandler(
283
286
  {
284
287
  onScroll: ({ contentOffset: { x, y }, contentSize: { height: contentHeight }, layoutMeasurement: { height: layoutHeight } }) => {
@@ -287,9 +290,9 @@ function useScrollHandler({ capturedGeneration, chipHidden, chipThreshold = 100,
287
290
  const target = remountSyncTarget.value;
288
291
  if (Math.abs(y - target) < REMOUNT_SYNC_TOLERANCE) {
289
292
  remountSyncTarget.value = null;
290
- if (onRemountSynced) runOnJS(onRemountSynced)(y);
293
+ if (onRemountSynced) runOnJS(callWorkletCallback)(remountSyncedKey, y);
291
294
  } else if (onRemountSyncRetry) {
292
- runOnJS(onRemountSyncRetry)(y);
295
+ runOnJS(callWorkletCallback)(remountSyncRetryKey, y);
293
296
  }
294
297
  return;
295
298
  }
@@ -373,21 +376,23 @@ import { useAnimatedStyle, useSharedValue as useSharedValue3, withTiming as with
373
376
  import { useSafeAreaInsets } from "react-native-safe-area-context";
374
377
 
375
378
  // src/useKeyboardInset.ts
379
+ import { callWorkletCallback as callWorkletCallback2, useWorkletCallback as useWorkletCallback2 } from "@rific/core";
376
380
  import { useState as useState2 } from "react";
377
381
  import { Platform as Platform2 } from "react-native";
378
382
  import { useKeyboardHandler } from "react-native-keyboard-controller";
379
383
  import { runOnJS as runOnJS2 } from "react-native-reanimated";
380
384
  var useKeyboardInsetNative = () => {
381
385
  const [keyboardHeight, setKeyboardHeight] = useState2(0);
386
+ const keyboardHeightKey = useWorkletCallback2(setKeyboardHeight);
382
387
  useKeyboardHandler(
383
388
  {
384
389
  onMove: (e) => {
385
390
  "worklet";
386
- runOnJS2(setKeyboardHeight)(e.height);
391
+ runOnJS2(callWorkletCallback2)(keyboardHeightKey, e.height);
387
392
  },
388
393
  onEnd: (e) => {
389
394
  "worklet";
390
- runOnJS2(setKeyboardHeight)(e.height);
395
+ runOnJS2(callWorkletCallback2)(keyboardHeightKey, e.height);
391
396
  }
392
397
  },
393
398
  []
@@ -870,6 +875,7 @@ var styles4 = { measureContainer: { left: 0, opacity: 0, position: "absolute", r
870
875
  var FlatList = memo2(FlatListInner);
871
876
 
872
877
  // src/PullSearch.tsx
878
+ import { callWorkletCallback as callWorkletCallback3, useWorkletCallback as useWorkletCallback3 } from "@rific/core";
873
879
  import { forwardRef, useCallback as useCallback5, useContext as useContext8, useEffect as useEffect3, useImperativeHandle, useRef as useRef5, useState as useState4 } from "react";
874
880
  import { StyleSheet as StyleSheet4 } from "react-native";
875
881
  import { Searchbar, useTheme as useTheme3 } from "react-native-paper";
@@ -892,13 +898,14 @@ var PullSearch = forwardRef(function PullSearch2({ debounce, onChangeText, onHei
892
898
  focus: () => inputRef.current?.focus()
893
899
  }));
894
900
  const blurInput = useCallback5(() => inputRef.current?.blur(), []);
901
+ const blurKey = useWorkletCallback3(blurInput);
895
902
  useAnimatedReaction(
896
903
  () => {
897
904
  if (barHeight === 0) return false;
898
905
  return scrollPosition.value >= -(headerHeight ?? 0) + barHeight;
899
906
  },
900
907
  (isHidden, wasHidden) => {
901
- if (isHidden && !wasHidden) runOnJS3(blurInput)();
908
+ if (isHidden && !wasHidden) runOnJS3(callWorkletCallback3)(blurKey);
902
909
  },
903
910
  // scrollPosition has to be listed too, not just read - see useStickyHeaders' useAnimatedReaction
904
911
  // for why: on web, without reanimated's Babel plugin active, an unlisted SharedValue mutation
@@ -1075,6 +1082,13 @@ var ScrollViewChromeProvider = ({ children, footprint, overhang = 0, snapBack =
1075
1082
  return /* @__PURE__ */ jsx8(ScrollViewChromeContext.Provider, { value: chrome, children: /* @__PURE__ */ jsx8(ScrollViewChromeHostContext.Provider, { value: host, children }) });
1076
1083
  };
1077
1084
 
1085
+ // src/ScrollViewConfig.tsx
1086
+ import { createModuleConfig } from "@rific/core";
1087
+ var scrollViewConfig = createModuleConfig();
1088
+ var configureScrollView = scrollViewConfig.configure;
1089
+ var getScrollViewConfig = scrollViewConfig.getConfig;
1090
+ var ScrollViewConfigProvider = scrollViewConfig.Provider;
1091
+
1078
1092
  // src/ScrollViewFooter.tsx
1079
1093
  import { BlurView as BlurView2 } from "@rific/auto-paper";
1080
1094
  import { useContext as useContext10, useEffect as useEffect5, useLayoutEffect as useLayoutEffect5, useRef as useRef7 } from "react";
@@ -1197,6 +1211,8 @@ var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionAc
1197
1211
  }, [headerFixed, headerHeightShared, top, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1198
1212
  const actionTop = top + actionMargin;
1199
1213
  const buttonStyle = { borderRadius: actionSize, height: actionSize, margin: 0, width: actionSize };
1214
+ const BackAction = getScrollViewConfig().BackAction ?? Appbar.BackAction;
1215
+ const backActionElement = typeof backAction === "function" ? /* @__PURE__ */ jsx10(BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle }) : backAction;
1200
1216
  const actionShadow = { borderRadius: actionSize / 2, elevation: 2, shadowColor: "#000", shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.15, shadowRadius: 2 };
1201
1217
  const leadingStyle = { alignItems: "center", height: actionSize, justifyContent: "center", left: actionMargin, position: "absolute", top: actionTop, width: actionSize, zIndex: 3, ...actionShadow };
1202
1218
  const trailingStyle = { alignItems: "center", height: actionSize, justifyContent: "center", position: "absolute", right: actionMargin, top: actionTop, width: actionSize, zIndex: 3, ...actionShadow };
@@ -1216,10 +1232,10 @@ var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionAc
1216
1232
  /* @__PURE__ */ jsx10(Animated6.View, { pointerEvents: "none", style: [styles7.progress, progressStyle], children: /* @__PURE__ */ jsx10(ProgressBar, { indeterminate: progress === null, visible: progressing, progress: progress ?? void 0 }) }),
1217
1233
  backAction && (effectiveBackActionFixed ? /* @__PURE__ */ jsxs6(Animated6.View, { style: leadingStyle, children: [
1218
1234
  /* @__PURE__ */ jsx10(ActionBg, { blur, style: actionStyle }),
1219
- typeof backAction === "function" ? /* @__PURE__ */ jsx10(Appbar.BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle }) : backAction
1235
+ backActionElement
1220
1236
  ] }) : /* @__PURE__ */ jsxs6(Animated6.View, { style: [leadingStyle, translateStyle], children: [
1221
1237
  /* @__PURE__ */ jsx10(ActionBg, { blur, style: actionStyle }),
1222
- typeof backAction === "function" ? /* @__PURE__ */ jsx10(Appbar.BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle }) : backAction
1238
+ backActionElement
1223
1239
  ] })),
1224
1240
  trailingAction && (trailingActionFixed ? /* @__PURE__ */ jsxs6(View6, { style: trailingStyle, children: [
1225
1241
  /* @__PURE__ */ jsx10(ActionBg, { blur, style: actionStyle }),
@@ -1385,6 +1401,7 @@ import { Gesture as Gesture4, GestureDetector as GestureDetector4 } from "react-
1385
1401
  import Animated7, { runOnUI as runOnUI3, useSharedValue as useSharedValue10 } from "react-native-reanimated";
1386
1402
 
1387
1403
  // src/internal/useStickyHeaders.ts
1404
+ import { callWorkletCallback as callWorkletCallback4, useWorkletCallback as useWorkletCallback4 } from "@rific/core";
1388
1405
  import { useCallback as useCallback9, useContext as useContext13, useLayoutEffect as useLayoutEffect8, useRef as useRef10, useState as useState7 } from "react";
1389
1406
  import { runOnJS as runOnJS4, useAnimatedReaction as useAnimatedReaction2, useAnimatedStyle as useAnimatedStyle6, useSharedValue as useSharedValue9 } from "react-native-reanimated";
1390
1407
  import { useSafeAreaInsets as useSafeAreaInsets5 } from "react-native-safe-area-context";
@@ -1394,6 +1411,7 @@ function useStickyHeaders(headerFixed, enabled) {
1394
1411
  const sectionYPositions = useSharedValue9([]);
1395
1412
  const sectionHeights = useSharedValue9([]);
1396
1413
  const [activeIndex, setActiveIndex] = useState7(-1);
1414
+ const activeIndexKey = useWorkletCallback4(setActiveIndex);
1397
1415
  const contentIndexShared = useSharedValue9(-1);
1398
1416
  useLayoutEffect8(() => {
1399
1417
  contentIndexShared.value = activeIndex;
@@ -1425,7 +1443,7 @@ function useStickyHeaders(headerFixed, enabled) {
1425
1443
  return active;
1426
1444
  },
1427
1445
  (curr, prev) => {
1428
- if (curr !== prev) runOnJS4(setActiveIndex)(curr);
1446
+ if (curr !== prev) runOnJS4(callWorkletCallback4)(activeIndexKey, curr);
1429
1447
  },
1430
1448
  // Same web-only reanimated gap as useAnimatedStyle elsewhere in this file: this "prepare"
1431
1449
  // function's own SharedValue reads (sectionYPositions, headerHeightShared, scrollPosition,
@@ -1686,6 +1704,7 @@ export {
1686
1704
  PullSearch,
1687
1705
  ScrollView,
1688
1706
  ScrollViewChromeProvider,
1707
+ ScrollViewConfigProvider,
1689
1708
  ScrollViewContext,
1690
1709
  ScrollViewFooter,
1691
1710
  ScrollViewHeader,
@@ -1693,7 +1712,9 @@ export {
1693
1712
  ScrollViewSettingsContext,
1694
1713
  ScrollViewSettingsProvider,
1695
1714
  SectionList,
1715
+ configureScrollView,
1696
1716
  defaultScrollViewSettings,
1717
+ getScrollViewConfig,
1697
1718
  scrollViewActions,
1698
1719
  scrollViewReducer,
1699
1720
  useKeyboardInset,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rific/scroll-view",
3
- "version": "0.9.2",
3
+ "version": "1.1.0",
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",
@@ -66,7 +66,7 @@
66
66
  "@infinitetoken/tsconfig": "^0.6.0",
67
67
  "@reduxjs/toolkit": "^2.12.0",
68
68
  "@rific/auto-paper": "^0.13.3",
69
- "@rific/core": "^0.3.1",
69
+ "@rific/core": "^0.5.0",
70
70
  "@testing-library/dom": "^10.4.1",
71
71
  "@testing-library/react": "^16.3.2",
72
72
  "@types/jest": "^30.0.0",
@@ -92,7 +92,7 @@
92
92
  },
93
93
  "peerDependencies": {
94
94
  "@rific/auto-paper": ">=0.10.0",
95
- "@rific/core": ">=0.1.1",
95
+ "@rific/core": ">=0.5.0",
96
96
  "react": ">=19.0.0",
97
97
  "react-native": ">=0.83.0",
98
98
  "react-native-gesture-handler": ">=2.0.0 <4.0.0",
@@ -1,3 +1,4 @@
1
+ import { callWorkletCallback, useWorkletCallback } from '@rific/core'
1
2
  import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef, useState } from 'react'
2
3
  import { type LayoutChangeEvent, StyleSheet, type TextInput } from 'react-native'
3
4
  import { Searchbar, useTheme } from 'react-native-paper'
@@ -38,6 +39,7 @@ export const PullSearch = forwardRef<PullSearchHandle, PullSearchProps>(function
38
39
  }))
39
40
 
40
41
  const blurInput = useCallback(() => inputRef.current?.blur(), [])
42
+ const blurKey = useWorkletCallback(blurInput)
41
43
 
42
44
  useAnimatedReaction(
43
45
  () => {
@@ -45,7 +47,7 @@ export const PullSearch = forwardRef<PullSearchHandle, PullSearchProps>(function
45
47
  return scrollPosition.value >= -(headerHeight ?? 0) + barHeight
46
48
  },
47
49
  (isHidden, wasHidden) => {
48
- if (isHidden && !wasHidden) runOnJS(blurInput)()
50
+ if (isHidden && !wasHidden) runOnJS(callWorkletCallback)(blurKey)
49
51
  },
50
52
  // scrollPosition has to be listed too, not just read - see useStickyHeaders' useAnimatedReaction
51
53
  // for why: on web, without reanimated's Babel plugin active, an unlisted SharedValue mutation
@@ -0,0 +1,34 @@
1
+ import { createModuleConfig } from '@rific/core'
2
+ import type { ComponentType, ReactNode } from 'react'
3
+ import type { ViewStyle } from 'react-native'
4
+
5
+ // What ScrollViewHeader passes the back action it renders for a plain `backAction` callback.
6
+ // react-native-paper's Appbar.BackAction (the default) takes these, and so does
7
+ // @rific/feedback-press's AppbarBackAction, which wraps it, so either drops in as is.
8
+ export type ScrollViewBackActionProps = {
9
+ accessibilityLabel?: string
10
+ onPress: () => void
11
+ size: number
12
+ style: ViewStyle
13
+ }
14
+
15
+ export type ScrollViewConfig = {
16
+ /** Component ScrollViewHeader renders when `backAction` is a plain callback. Pass `AppbarBackAction` from `@rific/feedback-press` to give every header's back button its haptics and sound; omit to keep react-native-paper's `Appbar.BackAction`. A `backAction` element on one header still replaces it there. */
17
+ BackAction?: ComponentType<ScrollViewBackActionProps>
18
+ }
19
+
20
+ // @rific/core's createModuleConfig() supplies the module-level config singleton (configure/
21
+ // getConfig/Provider) every @rific package wires up the same way - see its own doc comment for
22
+ // why this is plain module state rather than React Context. It injects a component instead of
23
+ // depending on @rific/feedback-press, same as @rific/resizable-input's TextInputComponent.
24
+ //
25
+ // Not to be confused with ScrollViewProvider (per screen, scroll state) or
26
+ // ScrollViewSettingsProvider (app-wide, persisted user settings): this one is app-wide startup
27
+ // config, mounted once.
28
+ const scrollViewConfig = createModuleConfig<ScrollViewConfig>()
29
+
30
+ export const configureScrollView = scrollViewConfig.configure
31
+ export const getScrollViewConfig = scrollViewConfig.getConfig
32
+ export const ScrollViewConfigProvider = scrollViewConfig.Provider
33
+
34
+ export type ScrollViewConfigProviderProps = ScrollViewConfig & { children: ReactNode }
@@ -5,13 +5,15 @@ import { Appbar, ProgressBar, Surface, useTheme } from 'react-native-paper'
5
5
  import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
6
6
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
7
7
 
8
+ import { getScrollViewConfig } from './ScrollViewConfig'
8
9
  import { ScrollViewContext } from './ScrollViewContext'
9
10
  import { ScrollViewSettingsContext } from './ScrollViewSettingsContext'
10
11
 
11
12
  export type ScrollViewHeaderProps = {
12
13
  actionSize?: number
13
14
  actionStyle?: ViewStyle
14
- // A plain callback renders the default Appbar.BackAction (themed onSurface, sized/positioned by
15
+ // A plain callback renders the default back action (Paper's Appbar.BackAction unless the app set
16
+ // another with configureScrollView({ BackAction }), themed onSurface, sized/positioned by
15
17
  // actionSize/iconSize below) - the common case, unchanged from before. Passing a ReactNode
16
18
  // instead skips that default entirely and renders the given element in its place, same escape
17
19
  // hatch trailingAction already gave the other corner: a caller that wants its own color, icon,
@@ -131,6 +133,10 @@ export const ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, bac
131
133
  }, [headerFixed, headerHeightShared, top, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared])
132
134
  const actionTop = top + actionMargin
133
135
  const buttonStyle = { borderRadius: actionSize, height: actionSize, margin: 0, width: actionSize }
136
+ // A plain callback renders the configured BackAction (configureScrollView / ScrollViewConfigProvider),
137
+ // Paper's Appbar.BackAction by default. Read at render, like every createModuleConfig consumer.
138
+ const BackAction = getScrollViewConfig().BackAction ?? Appbar.BackAction
139
+ const backActionElement = typeof backAction === 'function' ? <BackAction accessibilityLabel={backActionAccessibilityLabel} onPress={backAction} size={iconSize ?? actionSize / 2} style={buttonStyle} /> : backAction
134
140
  const actionShadow = { borderRadius: actionSize / 2, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.15, shadowRadius: 2 }
135
141
  const leadingStyle = { alignItems: 'center' as const, height: actionSize, justifyContent: 'center' as const, left: actionMargin, position: 'absolute' as const, top: actionTop, width: actionSize, zIndex: 3, ...actionShadow }
136
142
  const trailingStyle = { alignItems: 'center' as const, height: actionSize, justifyContent: 'center' as const, position: 'absolute' as const, right: actionMargin, top: actionTop, width: actionSize, zIndex: 3, ...actionShadow }
@@ -175,12 +181,12 @@ export const ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, bac
175
181
  (effectiveBackActionFixed ? (
176
182
  <Animated.View style={leadingStyle}>
177
183
  <ActionBg blur={blur} style={actionStyle} />
178
- {typeof backAction === 'function' ? <Appbar.BackAction accessibilityLabel={backActionAccessibilityLabel} onPress={backAction} size={iconSize ?? actionSize / 2} style={buttonStyle} /> : backAction}
184
+ {backActionElement}
179
185
  </Animated.View>
180
186
  ) : (
181
187
  <Animated.View style={[leadingStyle, translateStyle]}>
182
188
  <ActionBg blur={blur} style={actionStyle} />
183
- {typeof backAction === 'function' ? <Appbar.BackAction accessibilityLabel={backActionAccessibilityLabel} onPress={backAction} size={iconSize ?? actionSize / 2} style={buttonStyle} /> : backAction}
189
+ {backActionElement}
184
190
  </Animated.View>
185
191
  ))}
186
192
  {trailingAction &&
package/src/index.ts CHANGED
@@ -10,6 +10,8 @@ export type { ScrollViewProps } from './ScrollView'
10
10
  export { ScrollView } from './ScrollView'
11
11
  export type { ScrollViewChromeProviderProps } from './ScrollViewChromeProvider'
12
12
  export { ScrollViewChromeProvider } from './ScrollViewChromeProvider'
13
+ export type { ScrollViewBackActionProps, ScrollViewConfig, ScrollViewConfigProviderProps } from './ScrollViewConfig'
14
+ export { configureScrollView, getScrollViewConfig, ScrollViewConfigProvider } from './ScrollViewConfig'
13
15
  export type { ScrollViewContextType } from './ScrollViewContext'
14
16
  export { ScrollViewContext } from './ScrollViewContext'
15
17
  export type { ScrollViewFooterProps } from './ScrollViewFooter'
@@ -1,3 +1,4 @@
1
+ import { callWorkletCallback, useWorkletCallback } from '@rific/core'
1
2
  import { useContext } from 'react'
2
3
  import type { SharedValue } from 'react-native-reanimated'
3
4
  import { runOnJS, useAnimatedScrollHandler, useSharedValue, withTiming } from 'react-native-reanimated'
@@ -24,6 +25,10 @@ export function useScrollHandler({ capturedGeneration, chipHidden, chipThreshold
24
25
  const { chromeHosted, chromeWritable, footerOffset, headerHeightShared, headerOffset, pullSearchHeightShared, scrollPosition, snapBackFooterShared, snapBackHeaderShared, stackHeightShared } = useContext(ScrollViewContext)
25
26
  const snapUpAccum = useSharedValue(0)
26
27
  const fromBottomBounce = useSharedValue(false)
28
+ // Worklets never get the consumer's callbacks themselves: react-native-worklets holds a runOnJS target natively for as
29
+ // long as the worklet lives, which pinned the list. They call the module-level callWorkletCallback by key.
30
+ const remountSyncedKey = useWorkletCallback((y: number) => onRemountSynced?.(y))
31
+ const remountSyncRetryKey = useWorkletCallback((y: number) => onRemountSyncRetry?.(y))
27
32
 
28
33
  return useAnimatedScrollHandler(
29
34
  {
@@ -33,9 +38,9 @@ export function useScrollHandler({ capturedGeneration, chipHidden, chipThreshold
33
38
  const target = remountSyncTarget.value
34
39
  if (Math.abs(y - target) < REMOUNT_SYNC_TOLERANCE) {
35
40
  remountSyncTarget.value = null
36
- if (onRemountSynced) runOnJS(onRemountSynced)(y)
41
+ if (onRemountSynced) runOnJS(callWorkletCallback)(remountSyncedKey, y)
37
42
  } else if (onRemountSyncRetry) {
38
- runOnJS(onRemountSyncRetry)(y)
43
+ runOnJS(callWorkletCallback)(remountSyncRetryKey, y)
39
44
  }
40
45
  return
41
46
  }
@@ -1,3 +1,4 @@
1
+ import { callWorkletCallback, useWorkletCallback } from '@rific/core'
1
2
  import { useCallback, useContext, useLayoutEffect, useRef, useState } from 'react'
2
3
  import { runOnJS, useAnimatedReaction, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
3
4
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
@@ -19,6 +20,7 @@ export function useStickyHeaders(headerFixed: boolean, enabled: boolean): Sticky
19
20
  const sectionYPositions = useSharedValue<number[]>([])
20
21
  const sectionHeights = useSharedValue<number[]>([])
21
22
  const [activeIndex, setActiveIndex] = useState(-1)
23
+ const activeIndexKey = useWorkletCallback(setActiveIndex)
22
24
  // Mirrors activeIndex React state but lives on the UI thread for overlayStyle.
23
25
  // Updated via useLayoutEffect so content and style always change together.
24
26
  const contentIndexShared = useSharedValue(-1)
@@ -60,7 +62,7 @@ export function useStickyHeaders(headerFixed: boolean, enabled: boolean): Sticky
60
62
  return active
61
63
  },
62
64
  (curr, prev) => {
63
- if (curr !== prev) runOnJS(setActiveIndex)(curr)
65
+ if (curr !== prev) runOnJS(callWorkletCallback)(activeIndexKey, curr)
64
66
  },
65
67
  // Same web-only reanimated gap as useAnimatedStyle elsewhere in this file: this "prepare"
66
68
  // function's own SharedValue reads (sectionYPositions, headerHeightShared, scrollPosition,
@@ -1,3 +1,4 @@
1
+ import { callWorkletCallback, useWorkletCallback } from '@rific/core'
1
2
  import { useState } from 'react'
2
3
  import { Platform } from 'react-native'
3
4
  import { useKeyboardHandler } from 'react-native-keyboard-controller'
@@ -5,15 +6,16 @@ import { runOnJS } from 'react-native-reanimated'
5
6
 
6
7
  const useKeyboardInsetNative = () => {
7
8
  const [keyboardHeight, setKeyboardHeight] = useState(0)
9
+ const keyboardHeightKey = useWorkletCallback(setKeyboardHeight)
8
10
  useKeyboardHandler(
9
11
  {
10
12
  onMove: (e) => {
11
13
  'worklet'
12
- runOnJS(setKeyboardHeight)(e.height)
14
+ runOnJS(callWorkletCallback)(keyboardHeightKey, e.height)
13
15
  },
14
16
  onEnd: (e) => {
15
17
  'worklet'
16
- runOnJS(setKeyboardHeight)(e.height)
18
+ runOnJS(callWorkletCallback)(keyboardHeightKey, e.height)
17
19
  }
18
20
  },
19
21
  []