@peersahab/side-island 0.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.
@@ -0,0 +1,146 @@
1
+ import React from 'react';
2
+ import { FlatListProps, ViewStyle } from 'react-native';
3
+
4
+ type TeamMember = {
5
+ id: string;
6
+ name: string;
7
+ avatar?: string;
8
+ role: string;
9
+ status?: "active" | "inactive";
10
+ };
11
+ type SideIslandHaptics = {
12
+ /**
13
+ * Called when the island opens (expanded becomes true).
14
+ * Implement this using your own haptics library (e.g. expo-haptics).
15
+ */
16
+ onOpen?: () => void | Promise<void>;
17
+ /**
18
+ * Called when the island closes (expanded becomes false).
19
+ * Implement this using your own haptics library (e.g. expo-haptics).
20
+ */
21
+ onClose?: () => void | Promise<void>;
22
+ /**
23
+ * Called when the focused item changes while scrolling.
24
+ * Use this to trigger a "rigid" (or other) haptic in your app.
25
+ */
26
+ onFocusChange?: (info: {
27
+ index: number;
28
+ } | null) => void | Promise<void>;
29
+ };
30
+ type SideIslandPosition = "left" | "right";
31
+ type SideIslandConfig = {
32
+ /**
33
+ * Which side of the screen the island is pinned to.
34
+ * Default: "right"
35
+ */
36
+ position?: SideIslandPosition;
37
+ width?: number;
38
+ height?: number;
39
+ waveAmplitude?: number;
40
+ waveY1?: number;
41
+ waveY2?: number;
42
+ backgroundColor?: string;
43
+ handleWidth?: number;
44
+ topOffset?: number;
45
+ /**
46
+ * Optional haptics adapter. If provided, it will be used to trigger haptic feedback
47
+ * on island open/close without adding a hard dependency to any haptics library.
48
+ */
49
+ haptics?: SideIslandHaptics;
50
+ };
51
+ type SideIslandController = {
52
+ expanded: boolean;
53
+ setExpanded: (next: boolean) => void;
54
+ open: () => void;
55
+ close: () => void;
56
+ toggle: () => void;
57
+ config: SideIslandConfig;
58
+ };
59
+ type SideIslandProviderProps = {
60
+ children: React.ReactNode;
61
+ defaultExpanded?: boolean;
62
+ onExpandedChange?: (next: boolean) => void;
63
+ config?: SideIslandConfig;
64
+ value?: {
65
+ expanded: boolean;
66
+ setExpanded: (next: boolean) => void;
67
+ config?: SideIslandConfig;
68
+ };
69
+ };
70
+ type SideIslandProps<ItemT> = {
71
+ items: readonly ItemT[];
72
+ renderItem: (info: {
73
+ item: ItemT;
74
+ index: number;
75
+ }) => React.ReactElement | null;
76
+ keyExtractor?: (item: ItemT, index: number) => string;
77
+ listProps?: Omit<FlatListProps<ItemT>, "data" | "renderItem" | "keyExtractor">;
78
+ /**
79
+ * Called whenever the "focused" item changes as the user scrolls.
80
+ * Focus is determined by the item closest to the vertical center of the island.
81
+ * On first open, the island scrolls to focus the first item (index 0) centered.
82
+ * On subsequent opens, the island scrolls back to the last focused item.
83
+ */
84
+ onFocusedItemChange?: (info: {
85
+ item: ItemT;
86
+ index: number;
87
+ } | null) => void;
88
+ /**
89
+ * Which side of the screen the island is pinned to.
90
+ * Default: "right"
91
+ */
92
+ position?: SideIslandPosition;
93
+ width?: number;
94
+ height?: number;
95
+ waveAmplitude?: number;
96
+ waveY1?: number;
97
+ waveY2?: number;
98
+ backgroundColor?: string;
99
+ handleWidth?: number;
100
+ topOffset?: number;
101
+ /**
102
+ * Optional haptics adapter. If provided, it will be used to trigger haptic feedback
103
+ * on island open/close without adding a hard dependency to any haptics library.
104
+ */
105
+ haptics?: SideIslandHaptics;
106
+ style?: ViewStyle;
107
+ expanded?: boolean;
108
+ onToggleExpanded?: (next: boolean) => void;
109
+ defaultExpanded?: boolean;
110
+ /**
111
+ * Fired when the handle area is pressed.
112
+ * The island will still toggle expansion via controlled/provider/internal state.
113
+ */
114
+ onPress?: () => void;
115
+ /**
116
+ * Optional backdrop component that will fade into view when the island expands.
117
+ * Should cover the full screen and be positioned behind the island.
118
+ */
119
+ backdropComponent?: React.ReactElement;
120
+ /**
121
+ * Optional component to render details of the currently focused item.
122
+ * Displayed on top of the backdrop, opposite of the island:
123
+ * - position="right" => detail is to the left of the island
124
+ * - position="left" => detail is to the right of the island
125
+ * Receives the focused item info and can interact with the island.
126
+ */
127
+ renderFocusedItemDetail?: (info: {
128
+ item: ItemT;
129
+ index: number;
130
+ expanded: boolean;
131
+ setExpanded: (next: boolean) => void;
132
+ }) => React.ReactElement | null;
133
+ /**
134
+ * Horizontal gap between the focused item detail component and the island.
135
+ * Default: 16
136
+ */
137
+ focusedItemDetailGap?: number;
138
+ };
139
+
140
+ declare function SideIsland<ItemT>({ items, renderItem, keyExtractor, listProps, onFocusedItemChange, position, width, height, waveAmplitude, waveY1, waveY2, backgroundColor, handleWidth, topOffset, haptics, style, expanded, onToggleExpanded, defaultExpanded, onPress, backdropComponent, renderFocusedItemDetail, focusedItemDetailGap, }: SideIslandProps<ItemT>): React.JSX.Element;
141
+
142
+ declare function SideIslandProvider({ children, defaultExpanded, onExpandedChange, config, value, }: SideIslandProviderProps): React.JSX.Element;
143
+
144
+ declare function useSideIsland(): SideIslandController;
145
+
146
+ export { SideIsland, type SideIslandConfig, type SideIslandController, type SideIslandPosition, type SideIslandProps, SideIslandProvider, type SideIslandProviderProps, type TeamMember, useSideIsland };
package/dist/index.js ADDED
@@ -0,0 +1,536 @@
1
+ 'use strict';
2
+
3
+ var React2 = require('react');
4
+ var reactNative = require('react-native');
5
+ var Animated = require('react-native-reanimated');
6
+ var reactNativeSkia = require('@shopify/react-native-skia');
7
+
8
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
+
10
+ var React2__default = /*#__PURE__*/_interopDefault(React2);
11
+ var Animated__default = /*#__PURE__*/_interopDefault(Animated);
12
+
13
+ // src/side-island.tsx
14
+ var SideIslandContext = React2.createContext(null);
15
+ function SideIslandProvider({
16
+ children,
17
+ defaultExpanded = false,
18
+ onExpandedChange,
19
+ config,
20
+ value
21
+ }) {
22
+ var _a;
23
+ const [internalExpanded, setInternalExpanded] = React2.useState(defaultExpanded);
24
+ const expanded = (_a = value == null ? void 0 : value.expanded) != null ? _a : internalExpanded;
25
+ const setExpanded = React2.useCallback(
26
+ (next) => {
27
+ if (value == null ? void 0 : value.setExpanded) {
28
+ value.setExpanded(next);
29
+ } else {
30
+ setInternalExpanded(next);
31
+ }
32
+ onExpandedChange == null ? void 0 : onExpandedChange(next);
33
+ },
34
+ [onExpandedChange, value]
35
+ );
36
+ const mergedConfig = React2.useMemo(() => {
37
+ var _a2;
38
+ return {
39
+ ...config != null ? config : {},
40
+ ...(_a2 = value == null ? void 0 : value.config) != null ? _a2 : {}
41
+ };
42
+ }, [config, value == null ? void 0 : value.config]);
43
+ const ctxValue = React2.useMemo(() => {
44
+ return { expanded, setExpanded, config: mergedConfig };
45
+ }, [expanded, mergedConfig, setExpanded]);
46
+ return /* @__PURE__ */ React2__default.default.createElement(SideIslandContext.Provider, { value: ctxValue }, children);
47
+ }
48
+
49
+ // src/side-island.tsx
50
+ function clamp(n, min, max) {
51
+ "worklet";
52
+ return Math.max(min, Math.min(max, n));
53
+ }
54
+ function buildWaveIslandPath(w, h, position, amp = 34, y1 = 0.28, y2 = 0.72) {
55
+ const p = reactNativeSkia.Skia.Path.Make();
56
+ const pinnedRight = position === "right";
57
+ const xPinned = pinnedRight ? w : 0;
58
+ const xBulge = pinnedRight ? 0 : w;
59
+ const minGap = 0.22;
60
+ const Y1 = h * clamp(y1, 0.05, 0.45);
61
+ const Y2 = h * clamp(y2, 0.55, 0.95);
62
+ const gap = Y2 - Y1;
63
+ const fixGap = gap < h * minGap;
64
+ const Y1Fixed = fixGap ? h * 0.5 - h * minGap / 2 : Y1;
65
+ const Y2Fixed = fixGap ? h * 0.5 + h * minGap / 2 : Y2;
66
+ const curveSize = Math.max(1, Math.min(Y1Fixed, h - Y2Fixed, w * 0.8));
67
+ p.moveTo(xPinned, 0);
68
+ p.lineTo(xPinned, h);
69
+ p.cubicTo(
70
+ xPinned,
71
+ h - curveSize * 0.75,
72
+ xBulge,
73
+ Y2Fixed + curveSize * 0.75,
74
+ xBulge,
75
+ Y2Fixed
76
+ );
77
+ p.lineTo(xBulge, Y1Fixed);
78
+ p.cubicTo(xBulge, Y1Fixed - curveSize * 0.5, xPinned, curveSize * 0.5, xPinned, 0);
79
+ p.close();
80
+ return p;
81
+ }
82
+ function DefaultSeparator({ height = 12 }) {
83
+ return /* @__PURE__ */ React2__default.default.createElement(reactNative.View, { style: { height } });
84
+ }
85
+ function ScaledItem({
86
+ index,
87
+ scrollY,
88
+ itemHeight,
89
+ viewportHeight,
90
+ separatorHeight,
91
+ children
92
+ }) {
93
+ const animatedStyle = Animated.useAnimatedStyle(() => {
94
+ const h = itemHeight.value;
95
+ if (h <= 0) return {};
96
+ const stride = h + separatorHeight;
97
+ const inset = Math.max(0, viewportHeight / 2 - h / 2);
98
+ const itemCenterY = inset + index * stride + h / 2;
99
+ const viewportCenterY = scrollY.value + viewportHeight / 2;
100
+ const dist = Math.abs(itemCenterY - viewportCenterY);
101
+ const scale = Animated.interpolate(dist, [0, stride, stride * 2, stride * 3], [1, 0.8, 0.5, 0], Animated.Extrapolation.CLAMP);
102
+ return { transform: [{ scale }] };
103
+ }, [index, separatorHeight, viewportHeight]);
104
+ return /* @__PURE__ */ React2__default.default.createElement(Animated__default.default.View, { style: [{ alignItems: "center", justifyContent: "center" }, animatedStyle] }, children);
105
+ }
106
+ function SideIsland({
107
+ items,
108
+ renderItem,
109
+ keyExtractor,
110
+ listProps,
111
+ onFocusedItemChange,
112
+ position,
113
+ width,
114
+ height,
115
+ waveAmplitude,
116
+ waveY1,
117
+ waveY2,
118
+ backgroundColor,
119
+ handleWidth,
120
+ topOffset,
121
+ haptics,
122
+ style,
123
+ expanded,
124
+ onToggleExpanded,
125
+ defaultExpanded = false,
126
+ onPress,
127
+ backdropComponent,
128
+ renderFocusedItemDetail,
129
+ focusedItemDetailGap = 16
130
+ }) {
131
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
132
+ const { height: screenHeight, width: screenWidth } = reactNative.useWindowDimensions();
133
+ reactNative.useColorScheme();
134
+ const ctx = React2__default.default.useContext(SideIslandContext);
135
+ const resolvedPosition = (_a = position != null ? position : ctx == null ? void 0 : ctx.config.position) != null ? _a : "right";
136
+ const resolvedWidth = (_b = width != null ? width : ctx == null ? void 0 : ctx.config.width) != null ? _b : 40;
137
+ const resolvedHeight = (_c = height != null ? height : ctx == null ? void 0 : ctx.config.height) != null ? _c : 250;
138
+ const resolvedWaveAmplitude = (_d = waveAmplitude != null ? waveAmplitude : ctx == null ? void 0 : ctx.config.waveAmplitude) != null ? _d : 18;
139
+ const resolvedWaveY1 = (_e = waveY1 != null ? waveY1 : ctx == null ? void 0 : ctx.config.waveY1) != null ? _e : 0.1;
140
+ const resolvedWaveY2 = (_f = waveY2 != null ? waveY2 : ctx == null ? void 0 : ctx.config.waveY2) != null ? _f : 0.9;
141
+ const resolvedHandleWidth = (_g = handleWidth != null ? handleWidth : ctx == null ? void 0 : ctx.config.handleWidth) != null ? _g : 16;
142
+ const resolvedTopOffset = (_h = topOffset != null ? topOffset : ctx == null ? void 0 : ctx.config.topOffset) != null ? _h : 0;
143
+ const resolvedHaptics = haptics != null ? haptics : ctx == null ? void 0 : ctx.config.haptics;
144
+ const defaultBg = "#000000";
145
+ const resolvedBackgroundColor = (_i = backgroundColor != null ? backgroundColor : ctx == null ? void 0 : ctx.config.backgroundColor) != null ? _i : defaultBg;
146
+ const [internalExpanded, setInternalExpanded] = React2.useState(defaultExpanded);
147
+ const isControlled = typeof expanded === "boolean";
148
+ const isUsingProvider = !!ctx && !isControlled;
149
+ const effectiveExpanded = isControlled ? expanded : isUsingProvider ? ctx.expanded : internalExpanded;
150
+ const prevExpandedRef = React2.useRef(effectiveExpanded);
151
+ React2.useEffect(() => {
152
+ var _a2, _b2;
153
+ const prev = prevExpandedRef.current;
154
+ if (prev === effectiveExpanded) return;
155
+ prevExpandedRef.current = effectiveExpanded;
156
+ if (!resolvedHaptics) return;
157
+ if (effectiveExpanded) {
158
+ try {
159
+ void ((_a2 = resolvedHaptics.onOpen) == null ? void 0 : _a2.call(resolvedHaptics));
160
+ } catch (e) {
161
+ }
162
+ } else {
163
+ try {
164
+ void ((_b2 = resolvedHaptics.onClose) == null ? void 0 : _b2.call(resolvedHaptics));
165
+ } catch (e) {
166
+ }
167
+ }
168
+ }, [effectiveExpanded, resolvedHaptics]);
169
+ const setExpanded = (next) => {
170
+ if (isControlled) {
171
+ onToggleExpanded == null ? void 0 : onToggleExpanded(next);
172
+ return;
173
+ }
174
+ if (isUsingProvider) {
175
+ ctx.setExpanded(next);
176
+ return;
177
+ }
178
+ setInternalExpanded(next);
179
+ };
180
+ const toggleExpanded = () => setExpanded(!effectiveExpanded);
181
+ const topPosition = Math.round((screenHeight - resolvedHeight) / 2) + resolvedTopOffset;
182
+ const collapsedMagnitude = Math.max(0, resolvedWidth - resolvedHandleWidth);
183
+ const collapsedTranslateX = resolvedPosition === "right" ? collapsedMagnitude : -collapsedMagnitude;
184
+ const translateXAnim = Animated.useSharedValue(collapsedTranslateX);
185
+ const backdropOpacity = Animated.useSharedValue(effectiveExpanded ? 1 : 0);
186
+ React2.useEffect(() => {
187
+ translateXAnim.value = Animated.withTiming(effectiveExpanded ? 0 : collapsedTranslateX, {
188
+ duration: 300,
189
+ easing: Animated.Easing.out(Animated.Easing.ease)
190
+ });
191
+ backdropOpacity.value = Animated.withTiming(effectiveExpanded ? 1 : 0, {
192
+ duration: 300,
193
+ easing: Animated.Easing.out(Animated.Easing.ease)
194
+ });
195
+ }, [collapsedTranslateX, effectiveExpanded, translateXAnim, backdropOpacity]);
196
+ const path = React2.useMemo(() => {
197
+ return buildWaveIslandPath(resolvedWidth, resolvedHeight, resolvedPosition, resolvedWaveAmplitude, resolvedWaveY1, resolvedWaveY2);
198
+ }, [resolvedHeight, resolvedPosition, resolvedWaveAmplitude, resolvedWaveY1, resolvedWaveY2, resolvedWidth]);
199
+ const animatedContainerStyle = Animated.useAnimatedStyle(() => {
200
+ return { transform: [{ translateX: translateXAnim.value }] };
201
+ });
202
+ const animatedBackdropStyle = Animated.useAnimatedStyle(() => {
203
+ return { opacity: backdropOpacity.value };
204
+ });
205
+ const resolvedKeyExtractor = React2.useMemo(() => {
206
+ return keyExtractor != null ? keyExtractor : ((_, index) => String(index));
207
+ }, [keyExtractor]);
208
+ const separator = (_j = listProps == null ? void 0 : listProps.ItemSeparatorComponent) != null ? _j : (() => /* @__PURE__ */ React2__default.default.createElement(DefaultSeparator, null));
209
+ const listViewportPaddingTop = 24;
210
+ const listViewportPaddingBottom = 24;
211
+ const lastFocusedKeyRef = React2.useRef(null);
212
+ const lastFocusedIndexRef = React2.useRef(null);
213
+ const hasOpenedOnceRef = React2.useRef(false);
214
+ const suppressFocusRef = React2.useRef(false);
215
+ const listRef = React2.useRef(null);
216
+ const scrollYRef = React2.useRef(0);
217
+ const rafScheduledRef = React2.useRef(false);
218
+ const [measuredItemHeight, setMeasuredItemHeight] = React2.useState(null);
219
+ const scrollYAnim = Animated.useSharedValue(0);
220
+ const itemHeightAnim = Animated.useSharedValue(0);
221
+ const [focusedItemInfo, setFocusedItemInfo] = React2.useState(null);
222
+ React2.useEffect(() => {
223
+ if (measuredItemHeight != null && measuredItemHeight > 0 && itemHeightAnim.value <= 0) {
224
+ itemHeightAnim.value = measuredItemHeight;
225
+ }
226
+ }, [measuredItemHeight, itemHeightAnim]);
227
+ const contentEdgeInset = React2.useMemo(() => {
228
+ const viewportHeight = Math.max(0, resolvedHeight - listViewportPaddingTop - listViewportPaddingBottom);
229
+ const approxItemHalfHeight = measuredItemHeight ? measuredItemHeight / 2 : 20;
230
+ return Math.max(0, Math.round(viewportHeight / 2 - approxItemHalfHeight));
231
+ }, [measuredItemHeight, resolvedHeight]);
232
+ const defaultViewabilityConfig = React2.useMemo(() => {
233
+ return {
234
+ // Lower threshold so focus transitions don't require "half an item" to leave the viewport.
235
+ itemVisiblePercentThreshold: 10,
236
+ minimumViewTime: 0,
237
+ waitForInteraction: false
238
+ };
239
+ }, []);
240
+ const emitFocus = (index) => {
241
+ var _a2;
242
+ if (items.length === 0) return;
243
+ const maxIndex = Math.max(0, items.length - 1);
244
+ const clampedIndex = clamp(index, 0, maxIndex);
245
+ const focusedKey = String(clampedIndex);
246
+ if (focusedKey === lastFocusedKeyRef.current) return;
247
+ lastFocusedKeyRef.current = focusedKey;
248
+ lastFocusedIndexRef.current = clampedIndex;
249
+ const focusInfo = { item: items[clampedIndex], index: clampedIndex };
250
+ setFocusedItemInfo(focusInfo);
251
+ onFocusedItemChange == null ? void 0 : onFocusedItemChange(focusInfo);
252
+ if (effectiveExpanded) {
253
+ try {
254
+ void ((_a2 = resolvedHaptics == null ? void 0 : resolvedHaptics.onFocusChange) == null ? void 0 : _a2.call(resolvedHaptics, { index: clampedIndex }));
255
+ } catch (e) {
256
+ }
257
+ }
258
+ };
259
+ const recomputeFocus = () => {
260
+ if (suppressFocusRef.current) return;
261
+ if (items.length === 0) return;
262
+ const itemH = measuredItemHeight != null ? measuredItemHeight : 0;
263
+ if (itemH <= 0) return;
264
+ const separatorEstimate = (listProps == null ? void 0 : listProps.ItemSeparatorComponent) ? 0 : 12;
265
+ const stride = itemH + separatorEstimate;
266
+ const viewportHeight = Math.max(0, resolvedHeight - listViewportPaddingTop - listViewportPaddingBottom);
267
+ const centerY = scrollYRef.current + viewportHeight / 2;
268
+ const firstCenterY = contentEdgeInset + itemH / 2;
269
+ const raw = (centerY - firstCenterY) / stride;
270
+ const idx = Math.round(raw);
271
+ emitFocus(idx);
272
+ };
273
+ const handleViewableItemsChanged = React2.useMemo(() => {
274
+ return (info) => {
275
+ var _a2, _b2;
276
+ const viewable = ((_a2 = info == null ? void 0 : info.viewableItems) != null ? _a2 : []).filter((v) => v && v.index != null);
277
+ (_b2 = listProps == null ? void 0 : listProps.onViewableItemsChanged) == null ? void 0 : _b2.call(listProps, info);
278
+ const shouldReportFocus = !!onFocusedItemChange || !!(resolvedHaptics == null ? void 0 : resolvedHaptics.onFocusChange);
279
+ if (!shouldReportFocus) return;
280
+ if (suppressFocusRef.current) return;
281
+ if (viewable.length > 0) {
282
+ recomputeFocus();
283
+ }
284
+ };
285
+ }, [effectiveExpanded, listProps, onFocusedItemChange, resolvedHaptics]);
286
+ const scrollToIndexCentered = (index, animated) => {
287
+ var _a2;
288
+ const maxIndex = Math.max(0, items.length - 1);
289
+ const clamped = clamp(index, 0, maxIndex);
290
+ (_a2 = listRef.current) == null ? void 0 : _a2.scrollToIndex({ index: clamped, animated, viewPosition: 0.5 });
291
+ };
292
+ React2.useEffect(() => {
293
+ if (!effectiveExpanded) return;
294
+ if (items.length === 0) return;
295
+ const target = hasOpenedOnceRef.current && lastFocusedIndexRef.current != null ? lastFocusedIndexRef.current : 0;
296
+ suppressFocusRef.current = true;
297
+ const t0 = setTimeout(() => {
298
+ scrollToIndexCentered(target, false);
299
+ emitFocus(target);
300
+ const t1 = setTimeout(() => {
301
+ suppressFocusRef.current = false;
302
+ }, 50);
303
+ return () => clearTimeout(t1);
304
+ }, 0);
305
+ hasOpenedOnceRef.current = true;
306
+ return () => clearTimeout(t0);
307
+ }, [effectiveExpanded, items.length]);
308
+ const panResponder = React2.useMemo(() => {
309
+ return reactNative.PanResponder.create({
310
+ onMoveShouldSetPanResponder: (_evt, gestureState) => {
311
+ if (!effectiveExpanded) return false;
312
+ const dx = gestureState.dx;
313
+ const dy = gestureState.dy;
314
+ const should = resolvedPosition === "right" ? dx > 10 && Math.abs(dx) > Math.abs(dy) : dx < -10 && Math.abs(dx) > Math.abs(dy);
315
+ return should;
316
+ },
317
+ onPanResponderRelease: (_evt, gestureState) => {
318
+ if (!effectiveExpanded) return;
319
+ const dx = gestureState.dx;
320
+ const vx = gestureState.vx;
321
+ const shouldClose = resolvedPosition === "right" ? dx > 60 && vx > 0 : dx < -60 && vx < 0;
322
+ if (shouldClose) {
323
+ setExpanded(false);
324
+ }
325
+ },
326
+ onPanResponderTerminate: () => {
327
+ }
328
+ });
329
+ }, [effectiveExpanded, setExpanded, resolvedPosition]);
330
+ const panHandlers = backdropComponent ? void 0 : panResponder.panHandlers;
331
+ const islandContent = /* @__PURE__ */ React2__default.default.createElement(
332
+ Animated__default.default.View,
333
+ {
334
+ ...panHandlers,
335
+ style: [
336
+ styles.container,
337
+ {
338
+ width: resolvedWidth,
339
+ height: resolvedHeight,
340
+ top: topPosition,
341
+ ...resolvedPosition === "right" ? { right: 0 } : { left: 0 }
342
+ },
343
+ animatedContainerStyle,
344
+ style
345
+ ]
346
+ },
347
+ /* @__PURE__ */ React2__default.default.createElement(reactNative.View, { style: reactNative.StyleSheet.absoluteFill }, /* @__PURE__ */ React2__default.default.createElement(reactNativeSkia.Canvas, { style: reactNative.StyleSheet.absoluteFill, pointerEvents: "none" }, /* @__PURE__ */ React2__default.default.createElement(reactNativeSkia.Group, { clip: path }, /* @__PURE__ */ React2__default.default.createElement(reactNativeSkia.Path, { path, color: resolvedBackgroundColor }))), /* @__PURE__ */ React2__default.default.createElement(
348
+ reactNative.View,
349
+ {
350
+ style: [
351
+ reactNative.StyleSheet.absoluteFill,
352
+ resolvedPosition === "right" ? { paddingTop: listViewportPaddingTop, paddingBottom: listViewportPaddingBottom, paddingLeft: 6, paddingRight: 0, borderRadius: 50 } : { paddingTop: listViewportPaddingTop, paddingBottom: listViewportPaddingBottom, paddingLeft: 0, paddingRight: 6, borderRadius: 50 }
353
+ ],
354
+ pointerEvents: "box-none"
355
+ },
356
+ /* @__PURE__ */ React2__default.default.createElement(
357
+ reactNative.FlatList,
358
+ {
359
+ ...listProps,
360
+ ref: (r) => {
361
+ listRef.current = r;
362
+ },
363
+ data: items,
364
+ renderItem: ({ item, index }) => {
365
+ const viewportHeight = Math.max(0, resolvedHeight - listViewportPaddingTop - listViewportPaddingBottom);
366
+ const separatorHeight = (listProps == null ? void 0 : listProps.ItemSeparatorComponent) ? 0 : 12;
367
+ return /* @__PURE__ */ React2__default.default.createElement(
368
+ ScaledItem,
369
+ {
370
+ index,
371
+ scrollY: scrollYAnim,
372
+ itemHeight: itemHeightAnim,
373
+ viewportHeight,
374
+ separatorHeight
375
+ },
376
+ /* @__PURE__ */ React2__default.default.createElement(
377
+ reactNative.View,
378
+ {
379
+ onLayout: (e) => {
380
+ const { height: height2 } = e.nativeEvent.layout;
381
+ if (height2 > 0 && itemHeightAnim.value <= 0) {
382
+ itemHeightAnim.value = height2;
383
+ }
384
+ if (measuredItemHeight == null && height2 > 0) {
385
+ setMeasuredItemHeight(height2);
386
+ }
387
+ }
388
+ },
389
+ renderItem({ item, index })
390
+ )
391
+ );
392
+ },
393
+ keyExtractor: resolvedKeyExtractor,
394
+ showsVerticalScrollIndicator: (_k = listProps == null ? void 0 : listProps.showsVerticalScrollIndicator) != null ? _k : false,
395
+ ItemSeparatorComponent: separator,
396
+ viewabilityConfig: (_l = listProps == null ? void 0 : listProps.viewabilityConfig) != null ? _l : defaultViewabilityConfig,
397
+ onViewableItemsChanged: handleViewableItemsChanged,
398
+ onScroll: (e) => {
399
+ var _a2;
400
+ scrollYRef.current = e.nativeEvent.contentOffset.y;
401
+ scrollYAnim.value = e.nativeEvent.contentOffset.y;
402
+ (_a2 = listProps == null ? void 0 : listProps.onScroll) == null ? void 0 : _a2.call(listProps, e);
403
+ if (!rafScheduledRef.current) {
404
+ rafScheduledRef.current = true;
405
+ requestAnimationFrame(() => {
406
+ rafScheduledRef.current = false;
407
+ recomputeFocus();
408
+ });
409
+ }
410
+ },
411
+ scrollEventThrottle: (_m = listProps == null ? void 0 : listProps.scrollEventThrottle) != null ? _m : 16,
412
+ onScrollToIndexFailed: (info) => {
413
+ var _a2;
414
+ (_a2 = listProps == null ? void 0 : listProps.onScrollToIndexFailed) == null ? void 0 : _a2.call(listProps, info);
415
+ setTimeout(() => {
416
+ scrollToIndexCentered(info.index, false);
417
+ }, 50);
418
+ },
419
+ contentContainerStyle: [
420
+ { alignItems: "center", paddingTop: contentEdgeInset, paddingBottom: contentEdgeInset },
421
+ listProps == null ? void 0 : listProps.contentContainerStyle
422
+ ]
423
+ }
424
+ )
425
+ ), resolvedHandleWidth > 0 && /* @__PURE__ */ React2__default.default.createElement(
426
+ reactNative.Pressable,
427
+ {
428
+ onPress: () => {
429
+ onPress == null ? void 0 : onPress();
430
+ toggleExpanded();
431
+ },
432
+ style: [
433
+ styles.handle,
434
+ { width: resolvedHandleWidth },
435
+ resolvedPosition === "right" ? styles.handleRight : styles.handleLeft
436
+ ],
437
+ hitSlop: 10
438
+ }
439
+ ))
440
+ );
441
+ const focusedItemDetailContent = React2.useMemo(() => {
442
+ if (!renderFocusedItemDetail || !focusedItemInfo) return null;
443
+ return renderFocusedItemDetail({
444
+ item: focusedItemInfo.item,
445
+ index: focusedItemInfo.index,
446
+ expanded: effectiveExpanded,
447
+ setExpanded
448
+ });
449
+ }, [renderFocusedItemDetail, focusedItemInfo, effectiveExpanded, setExpanded]);
450
+ return /* @__PURE__ */ React2__default.default.createElement(React2__default.default.Fragment, null, backdropComponent && /* @__PURE__ */ React2__default.default.createElement(
451
+ Animated__default.default.View,
452
+ {
453
+ style: [
454
+ styles.backdrop,
455
+ {
456
+ width: screenWidth,
457
+ height: screenHeight
458
+ },
459
+ animatedBackdropStyle
460
+ ],
461
+ pointerEvents: effectiveExpanded ? "auto" : "none",
462
+ ...panResponder.panHandlers
463
+ },
464
+ backdropComponent
465
+ ), focusedItemDetailContent && /* @__PURE__ */ React2__default.default.createElement(
466
+ Animated__default.default.View,
467
+ {
468
+ style: [
469
+ styles.focusedItemDetail,
470
+ {
471
+ ...resolvedPosition === "right" ? { right: resolvedWidth + focusedItemDetailGap } : { left: resolvedWidth + focusedItemDetailGap },
472
+ top: topPosition,
473
+ height: resolvedHeight
474
+ },
475
+ animatedBackdropStyle
476
+ ],
477
+ pointerEvents: effectiveExpanded ? "auto" : "none"
478
+ },
479
+ focusedItemDetailContent
480
+ ), islandContent);
481
+ }
482
+ var styles = reactNative.StyleSheet.create({
483
+ backdrop: {
484
+ position: "absolute",
485
+ top: 0,
486
+ left: 0,
487
+ zIndex: 9998
488
+ },
489
+ container: {
490
+ position: "absolute",
491
+ zIndex: 9999
492
+ },
493
+ handle: {
494
+ position: "absolute",
495
+ top: 0,
496
+ bottom: 0,
497
+ backgroundColor: "transparent"
498
+ },
499
+ handleRight: {
500
+ right: 0
501
+ },
502
+ handleLeft: {
503
+ left: 0
504
+ },
505
+ focusedItemDetail: {
506
+ position: "absolute",
507
+ zIndex: 9999,
508
+ justifyContent: "center"
509
+ }
510
+ });
511
+ function useSideIsland() {
512
+ const ctx = React2.useContext(SideIslandContext);
513
+ if (!ctx) {
514
+ throw new Error("useSideIsland must be used within a SideIslandProvider");
515
+ }
516
+ const open = React2.useCallback(() => ctx.setExpanded(true), [ctx]);
517
+ const close = React2.useCallback(() => ctx.setExpanded(false), [ctx]);
518
+ const toggle = React2.useCallback(() => ctx.setExpanded(!ctx.expanded), [ctx]);
519
+ return React2.useMemo(
520
+ () => ({
521
+ expanded: ctx.expanded,
522
+ setExpanded: ctx.setExpanded,
523
+ open,
524
+ close,
525
+ toggle,
526
+ config: ctx.config
527
+ }),
528
+ [close, ctx.config, ctx.expanded, ctx.setExpanded, open, toggle]
529
+ );
530
+ }
531
+
532
+ exports.SideIsland = SideIsland;
533
+ exports.SideIslandProvider = SideIslandProvider;
534
+ exports.useSideIsland = useSideIsland;
535
+ //# sourceMappingURL=index.js.map
536
+ //# sourceMappingURL=index.js.map