@retor/react-native 0.3.1 → 0.3.4

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
@@ -6,14 +6,16 @@ Embed Retor 3D experiences in a React Native / Expo app — with composable bott
6
6
 
7
7
  ```bash
8
8
  # Expo
9
- npx expo install react-native-webview react-native-gesture-handler react-native-reanimated react-native-svg
9
+ npx expo install react-native-webview react-native-gesture-handler react-native-reanimated react-native-svg expo-blur
10
10
  npm install @gorhom/bottom-sheet lucide-react-native @retor/react-native
11
11
 
12
12
  # bare React Native
13
- npm install react-native-webview react-native-gesture-handler react-native-reanimated react-native-svg @gorhom/bottom-sheet lucide-react-native @retor/react-native
13
+ npm install react-native-webview react-native-gesture-handler react-native-reanimated react-native-svg @gorhom/bottom-sheet lucide-react-native expo-blur @retor/react-native
14
14
  cd ios && pod install
15
15
  ```
16
16
 
17
+ > `expo-blur` is optional — it's used for the default blurred sheet background. Skip it if you don't want blur and pass a custom `backgroundComponent` to any sheet.
18
+
17
19
  You also need to wrap your app root in a `GestureHandlerRootView` (per `@gorhom/bottom-sheet` requirements):
18
20
 
19
21
  ```tsx
package/dist/index.d.mts CHANGED
@@ -185,7 +185,7 @@ interface HudProps {
185
185
  declare function Hud({ children }: HudProps): React.JSX.Element;
186
186
 
187
187
  interface ProjectSheetProps {
188
- /** Snap points. Defaults to ["35%", "85%"]. */
188
+ /** Snap points. Defaults to ["35%", "75%"]. */
189
189
  snapPoints?: (string | number)[];
190
190
  /** Override the default header (project name + description + arrow button). */
191
191
  renderHeader?: (project: {
@@ -207,7 +207,7 @@ interface LinesCarouselProps {
207
207
  declare function LinesCarousel({ children, gap, paddingHorizontal }: LinesCarouselProps): React.JSX.Element | null;
208
208
 
209
209
  interface LineDetailSheetProps {
210
- /** Snap points. Defaults to ["35%", "85%"]. */
210
+ /** Snap points. Defaults to ["35%", "75%"]. */
211
211
  snapPoints?: (string | number)[];
212
212
  /** Override the header. */
213
213
  renderHeader?: (line: RetorLine) => React.ReactNode;
@@ -220,6 +220,12 @@ interface LineTagListProps {
220
220
  /** Optional header rendered above the list (used internally for the default header). */
221
221
  listHeader?: React.ReactNode;
222
222
  }
223
+ /**
224
+ * Renders the tags of the active line as a vertical list inside a
225
+ * `BottomSheetScrollView`. Tracks each item's Y position via `onLayout`
226
+ * and scrolls the closest tag to the top of the visible area as the
227
+ * camera scrolls.
228
+ */
223
229
  declare function LineTagList({ children, listHeader }: LineTagListProps): React.JSX.Element | null;
224
230
 
225
231
  interface AddNoteSheetProps {
package/dist/index.d.ts CHANGED
@@ -185,7 +185,7 @@ interface HudProps {
185
185
  declare function Hud({ children }: HudProps): React.JSX.Element;
186
186
 
187
187
  interface ProjectSheetProps {
188
- /** Snap points. Defaults to ["35%", "85%"]. */
188
+ /** Snap points. Defaults to ["35%", "75%"]. */
189
189
  snapPoints?: (string | number)[];
190
190
  /** Override the default header (project name + description + arrow button). */
191
191
  renderHeader?: (project: {
@@ -207,7 +207,7 @@ interface LinesCarouselProps {
207
207
  declare function LinesCarousel({ children, gap, paddingHorizontal }: LinesCarouselProps): React.JSX.Element | null;
208
208
 
209
209
  interface LineDetailSheetProps {
210
- /** Snap points. Defaults to ["35%", "85%"]. */
210
+ /** Snap points. Defaults to ["35%", "75%"]. */
211
211
  snapPoints?: (string | number)[];
212
212
  /** Override the header. */
213
213
  renderHeader?: (line: RetorLine) => React.ReactNode;
@@ -220,6 +220,12 @@ interface LineTagListProps {
220
220
  /** Optional header rendered above the list (used internally for the default header). */
221
221
  listHeader?: React.ReactNode;
222
222
  }
223
+ /**
224
+ * Renders the tags of the active line as a vertical list inside a
225
+ * `BottomSheetScrollView`. Tracks each item's Y position via `onLayout`
226
+ * and scrolls the closest tag to the top of the visible area as the
227
+ * camera scrolls.
228
+ */
223
229
  declare function LineTagList({ children, listHeader }: LineTagListProps): React.JSX.Element | null;
224
230
 
225
231
  interface AddNoteSheetProps {
package/dist/index.js CHANGED
@@ -217,14 +217,10 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
217
217
  openLine: (lineId) => send("open-line", { lineId }),
218
218
  exitLine: () => send("exit-line"),
219
219
  scrollToTag: (tagId) => send("scroll-to-tag", { tagId }),
220
- toggleAutoplay: () => {
221
- setIsPlaying((v) => !v);
222
- send("toggle-autoplay");
223
- },
224
- setAutoplay: (playing) => {
225
- setIsPlaying(playing);
226
- send("set-autoplay", { playing });
227
- }
220
+ // Bridge owns autoplay state — it'll emit `autoplay-state` back, which
221
+ // we use to update isPlaying. No optimistic local update.
222
+ toggleAutoplay: () => send("toggle-autoplay"),
223
+ setAutoplay: (playing) => send("set-autoplay", { playing })
228
224
  }),
229
225
  [send]
230
226
  );
@@ -236,30 +232,47 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
236
232
  if (!notes || !readyRef.current) return;
237
233
  send("set-notes", { notes });
238
234
  }, [notes, send]);
235
+ const closestTagIdRef = (0, import_react2.useRef)(closestTagId);
236
+ const addNoteTagIdRef = (0, import_react2.useRef)(addNoteTagId);
237
+ const activeLineIdRef = (0, import_react2.useRef)(activeLineId);
238
+ const linesRef = (0, import_react2.useRef)(lines);
239
+ const onNoteSubmitRef = (0, import_react2.useRef)(onNoteSubmit);
240
+ (0, import_react2.useEffect)(() => {
241
+ closestTagIdRef.current = closestTagId;
242
+ }, [closestTagId]);
243
+ (0, import_react2.useEffect)(() => {
244
+ addNoteTagIdRef.current = addNoteTagId;
245
+ }, [addNoteTagId]);
246
+ (0, import_react2.useEffect)(() => {
247
+ activeLineIdRef.current = activeLineId;
248
+ }, [activeLineId]);
249
+ (0, import_react2.useEffect)(() => {
250
+ linesRef.current = lines;
251
+ }, [lines]);
252
+ (0, import_react2.useEffect)(() => {
253
+ onNoteSubmitRef.current = onNoteSubmit;
254
+ }, [onNoteSubmit]);
239
255
  const openAddNote = (0, import_react2.useCallback)((tagId) => {
240
- setAddNoteTagId(tagId ?? closestTagId ?? null);
256
+ setAddNoteTagId(tagId ?? closestTagIdRef.current ?? null);
241
257
  setIsAddNoteOpen(true);
242
- }, [closestTagId]);
258
+ }, []);
243
259
  const closeAddNote = (0, import_react2.useCallback)(() => {
244
260
  setIsAddNoteOpen(false);
245
261
  }, []);
246
- const submitNote = (0, import_react2.useCallback)(
247
- (text, isPrivate = true) => {
248
- const tagId = addNoteTagId;
249
- const lineId = activeLineId;
250
- const tag = lines.find((l) => l._id === lineId)?.tags.find((t) => t._id === tagId);
251
- const payload = {
252
- text,
253
- isPrivate,
254
- tagId,
255
- lineId,
256
- position: tag?.position ?? null
257
- };
258
- onNoteSubmit?.(payload);
259
- setIsAddNoteOpen(false);
260
- },
261
- [addNoteTagId, activeLineId, lines, onNoteSubmit]
262
- );
262
+ const submitNote = (0, import_react2.useCallback)((text, isPrivate = true) => {
263
+ const tagId = addNoteTagIdRef.current;
264
+ const lineId = activeLineIdRef.current;
265
+ const tag = linesRef.current.find((l) => l._id === lineId)?.tags.find((t) => t._id === tagId);
266
+ const payload = {
267
+ text,
268
+ isPrivate,
269
+ tagId,
270
+ lineId,
271
+ position: tag?.position ?? null
272
+ };
273
+ onNoteSubmitRef.current?.(payload);
274
+ setIsAddNoteOpen(false);
275
+ }, []);
263
276
  const handleMessage = (0, import_react2.useCallback)(
264
277
  (event) => {
265
278
  let data = null;
@@ -299,6 +312,11 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
299
312
  onLineProgress?.(payload);
300
313
  break;
301
314
  }
315
+ case "autoplay-state": {
316
+ const payload = data.payload;
317
+ setIsPlaying(!!payload.playing);
318
+ break;
319
+ }
302
320
  default:
303
321
  onMessage?.(data.type, data.payload);
304
322
  }
@@ -371,7 +389,7 @@ function BlurBackground({ style }) {
371
389
  }
372
390
 
373
391
  // src/ProjectSheet.tsx
374
- function ProjectSheet({ snapPoints = ["35%", "85%"], renderHeader, children }) {
392
+ function ProjectSheet({ snapPoints = ["35%", "75%"], renderHeader, children }) {
375
393
  const { project, activeLineId, isAddNoteOpen } = useRetorBridge();
376
394
  const sheetRef = (0, import_react5.useRef)(null);
377
395
  const snapPointsArr = (0, import_react5.useMemo)(() => snapPoints, [snapPoints]);
@@ -379,11 +397,15 @@ function ProjectSheet({ snapPoints = ["35%", "85%"], renderHeader, children }) {
379
397
  (0, import_react5.useEffect)(() => {
380
398
  if (activeLineId || isAddNoteOpen) {
381
399
  sheetRef.current?.dismiss();
382
- } else {
400
+ return;
401
+ }
402
+ const t = setTimeout(() => {
383
403
  sheetRef.current?.present();
404
+ sheetRef.current?.snapToIndex(snapPointsArr.length - 1);
384
405
  setMinimized(false);
385
- }
386
- }, [activeLineId, isAddNoteOpen]);
406
+ }, 80);
407
+ return () => clearTimeout(t);
408
+ }, [activeLineId, isAddNoteOpen, snapPointsArr.length]);
387
409
  const handleSheetChange = (index) => {
388
410
  setMinimized(index === 0);
389
411
  };
@@ -401,6 +423,7 @@ function ProjectSheet({ snapPoints = ["35%", "85%"], renderHeader, children }) {
401
423
  snapPoints: snapPointsArr,
402
424
  enablePanDownToClose: false,
403
425
  enableDismissOnClose: false,
426
+ enableOverDrag: false,
404
427
  onChange: handleSheetChange,
405
428
  backdropComponent: (props) => /* @__PURE__ */ import_react5.default.createElement(
406
429
  import_bottom_sheet2.BottomSheetBackdrop,
@@ -492,19 +515,23 @@ var import_react_native5 = require("react-native");
492
515
  var import_bottom_sheet3 = require("@gorhom/bottom-sheet");
493
516
  var import_react_native_svg = __toESM(require("react-native-svg"));
494
517
  var import_lucide_react_native2 = require("lucide-react-native");
495
- function LineDetailSheet({ snapPoints = ["35%", "85%"], renderHeader, children }) {
518
+ function LineDetailSheet({ snapPoints = ["35%", "75%"], renderHeader, children }) {
496
519
  const { activeLine, isAddNoteOpen, controls } = useRetorBridge();
497
520
  const sheetRef = (0, import_react6.useRef)(null);
498
521
  const snapPointsArr = (0, import_react6.useMemo)(() => snapPoints, [snapPoints]);
499
522
  const [minimized, setMinimized] = (0, import_react6.useState)(false);
500
523
  (0, import_react6.useEffect)(() => {
501
- if (activeLine && !isAddNoteOpen) {
502
- sheetRef.current?.present();
503
- setMinimized(false);
504
- } else {
524
+ if (!activeLine || isAddNoteOpen) {
505
525
  sheetRef.current?.dismiss();
526
+ return;
506
527
  }
507
- }, [activeLine, isAddNoteOpen]);
528
+ const t = setTimeout(() => {
529
+ sheetRef.current?.present();
530
+ sheetRef.current?.snapToIndex(snapPointsArr.length - 1);
531
+ setMinimized(false);
532
+ }, 80);
533
+ return () => clearTimeout(t);
534
+ }, [activeLine, isAddNoteOpen, snapPointsArr.length]);
508
535
  const handleSheetChange = (index) => {
509
536
  setMinimized(index === 0);
510
537
  };
@@ -527,6 +554,7 @@ function LineDetailSheet({ snapPoints = ["35%", "85%"], renderHeader, children }
527
554
  snapPoints: snapPointsArr,
528
555
  enablePanDownToClose: false,
529
556
  enableDismissOnClose: false,
557
+ enableOverDrag: false,
530
558
  onChange: handleSheetChange,
531
559
  footerComponent: renderFooter,
532
560
  backdropComponent: (props) => /* @__PURE__ */ import_react6.default.createElement(
@@ -574,45 +602,37 @@ function AutoplayButton() {
574
602
  }
575
603
  function LineTagList({ children, listHeader }) {
576
604
  const { activeLine, closestTagId } = useRetorBridge();
577
- const listRef = (0, import_react6.useRef)(null);
605
+ const scrollRef = (0, import_react6.useRef)(null);
606
+ const offsetsRef = (0, import_react6.useRef)(/* @__PURE__ */ new Map());
578
607
  const tags = (0, import_react6.useMemo)(
579
608
  () => (activeLine?.tags ?? []).filter((t) => t.name && t.name.trim().length > 0),
580
609
  [activeLine]
581
610
  );
611
+ (0, import_react6.useEffect)(() => {
612
+ offsetsRef.current = /* @__PURE__ */ new Map();
613
+ }, [activeLine?._id]);
582
614
  (0, import_react6.useEffect)(() => {
583
615
  if (!closestTagId) return;
584
- const index = tags.findIndex((t2) => t2._id === closestTagId);
585
- if (index < 0) return;
586
616
  const t = setTimeout(() => {
587
- try {
588
- listRef.current?.scrollToIndex({ index, animated: true, viewPosition: 0 });
589
- } catch {
617
+ const y = offsetsRef.current.get(closestTagId);
618
+ if (y != null) {
619
+ scrollRef.current?.scrollTo({ y, animated: true });
590
620
  }
591
- }, 50);
621
+ }, 60);
592
622
  return () => clearTimeout(t);
593
- }, [closestTagId, tags]);
623
+ }, [closestTagId]);
594
624
  if (!activeLine) return null;
625
+ const handleItemLayout = (id, e) => {
626
+ offsetsRef.current.set(id, e.nativeEvent.layout.y);
627
+ };
595
628
  return /* @__PURE__ */ import_react6.default.createElement(
596
- import_bottom_sheet3.BottomSheetFlatList,
629
+ import_bottom_sheet3.BottomSheetScrollView,
597
630
  {
598
- ref: listRef,
599
- data: tags,
600
- keyExtractor: (t) => t._id,
601
- ListHeaderComponent: listHeader ? () => /* @__PURE__ */ import_react6.default.createElement(import_react6.default.Fragment, null, listHeader) : void 0,
602
- renderItem: ({ item }) => /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, null, children(item, item._id === closestTagId)),
603
- contentContainerStyle: styles3.list,
604
- removeClippedSubviews: false,
605
- onScrollToIndexFailed: (info) => {
606
- const offset = info.averageItemLength * info.index;
607
- listRef.current?.scrollToOffset({ offset, animated: false });
608
- setTimeout(() => {
609
- try {
610
- listRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0 });
611
- } catch {
612
- }
613
- }, 100);
614
- }
615
- }
631
+ ref: scrollRef,
632
+ contentContainerStyle: styles3.list
633
+ },
634
+ listHeader,
635
+ tags.map((tag) => /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { key: tag._id, onLayout: (e) => handleItemLayout(tag._id, e) }, children(tag, tag._id === closestTagId)))
616
636
  );
617
637
  }
618
638
  function DefaultLineTagList({ listHeader }) {
@@ -664,10 +684,11 @@ var styles3 = import_react_native5.StyleSheet.create({
664
684
  justifyContent: "center",
665
685
  position: "relative"
666
686
  },
667
- list: { paddingHorizontal: 16, paddingBottom: 96, gap: 4 },
687
+ list: { paddingBottom: 96, gap: 4 },
668
688
  tagItem: {
669
689
  flexDirection: "row",
670
690
  alignItems: "center",
691
+ marginHorizontal: 16,
671
692
  paddingHorizontal: 12,
672
693
  paddingVertical: 12,
673
694
  borderRadius: 12,
@@ -752,6 +773,7 @@ function AddNoteSheet({
752
773
  ref: sheetRef,
753
774
  snapPoints: snapPointsArr,
754
775
  enablePanDownToClose: true,
776
+ enableOverDrag: false,
755
777
  onDismiss: closeAddNote,
756
778
  backdropComponent: (props) => /* @__PURE__ */ import_react7.default.createElement(
757
779
  import_bottom_sheet4.BottomSheetBackdrop,
package/dist/index.mjs CHANGED
@@ -173,14 +173,10 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
173
173
  openLine: (lineId) => send("open-line", { lineId }),
174
174
  exitLine: () => send("exit-line"),
175
175
  scrollToTag: (tagId) => send("scroll-to-tag", { tagId }),
176
- toggleAutoplay: () => {
177
- setIsPlaying((v) => !v);
178
- send("toggle-autoplay");
179
- },
180
- setAutoplay: (playing) => {
181
- setIsPlaying(playing);
182
- send("set-autoplay", { playing });
183
- }
176
+ // Bridge owns autoplay state — it'll emit `autoplay-state` back, which
177
+ // we use to update isPlaying. No optimistic local update.
178
+ toggleAutoplay: () => send("toggle-autoplay"),
179
+ setAutoplay: (playing) => send("set-autoplay", { playing })
184
180
  }),
185
181
  [send]
186
182
  );
@@ -192,30 +188,47 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
192
188
  if (!notes || !readyRef.current) return;
193
189
  send("set-notes", { notes });
194
190
  }, [notes, send]);
191
+ const closestTagIdRef = useRef(closestTagId);
192
+ const addNoteTagIdRef = useRef(addNoteTagId);
193
+ const activeLineIdRef = useRef(activeLineId);
194
+ const linesRef = useRef(lines);
195
+ const onNoteSubmitRef = useRef(onNoteSubmit);
196
+ useEffect(() => {
197
+ closestTagIdRef.current = closestTagId;
198
+ }, [closestTagId]);
199
+ useEffect(() => {
200
+ addNoteTagIdRef.current = addNoteTagId;
201
+ }, [addNoteTagId]);
202
+ useEffect(() => {
203
+ activeLineIdRef.current = activeLineId;
204
+ }, [activeLineId]);
205
+ useEffect(() => {
206
+ linesRef.current = lines;
207
+ }, [lines]);
208
+ useEffect(() => {
209
+ onNoteSubmitRef.current = onNoteSubmit;
210
+ }, [onNoteSubmit]);
195
211
  const openAddNote = useCallback((tagId) => {
196
- setAddNoteTagId(tagId ?? closestTagId ?? null);
212
+ setAddNoteTagId(tagId ?? closestTagIdRef.current ?? null);
197
213
  setIsAddNoteOpen(true);
198
- }, [closestTagId]);
214
+ }, []);
199
215
  const closeAddNote = useCallback(() => {
200
216
  setIsAddNoteOpen(false);
201
217
  }, []);
202
- const submitNote = useCallback(
203
- (text, isPrivate = true) => {
204
- const tagId = addNoteTagId;
205
- const lineId = activeLineId;
206
- const tag = lines.find((l) => l._id === lineId)?.tags.find((t) => t._id === tagId);
207
- const payload = {
208
- text,
209
- isPrivate,
210
- tagId,
211
- lineId,
212
- position: tag?.position ?? null
213
- };
214
- onNoteSubmit?.(payload);
215
- setIsAddNoteOpen(false);
216
- },
217
- [addNoteTagId, activeLineId, lines, onNoteSubmit]
218
- );
218
+ const submitNote = useCallback((text, isPrivate = true) => {
219
+ const tagId = addNoteTagIdRef.current;
220
+ const lineId = activeLineIdRef.current;
221
+ const tag = linesRef.current.find((l) => l._id === lineId)?.tags.find((t) => t._id === tagId);
222
+ const payload = {
223
+ text,
224
+ isPrivate,
225
+ tagId,
226
+ lineId,
227
+ position: tag?.position ?? null
228
+ };
229
+ onNoteSubmitRef.current?.(payload);
230
+ setIsAddNoteOpen(false);
231
+ }, []);
219
232
  const handleMessage = useCallback(
220
233
  (event) => {
221
234
  let data = null;
@@ -255,6 +268,11 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
255
268
  onLineProgress?.(payload);
256
269
  break;
257
270
  }
271
+ case "autoplay-state": {
272
+ const payload = data.payload;
273
+ setIsPlaying(!!payload.playing);
274
+ break;
275
+ }
258
276
  default:
259
277
  onMessage?.(data.type, data.payload);
260
278
  }
@@ -327,7 +345,7 @@ function BlurBackground({ style }) {
327
345
  }
328
346
 
329
347
  // src/ProjectSheet.tsx
330
- function ProjectSheet({ snapPoints = ["35%", "85%"], renderHeader, children }) {
348
+ function ProjectSheet({ snapPoints = ["35%", "75%"], renderHeader, children }) {
331
349
  const { project, activeLineId, isAddNoteOpen } = useRetorBridge();
332
350
  const sheetRef = useRef2(null);
333
351
  const snapPointsArr = useMemo3(() => snapPoints, [snapPoints]);
@@ -335,11 +353,15 @@ function ProjectSheet({ snapPoints = ["35%", "85%"], renderHeader, children }) {
335
353
  useEffect2(() => {
336
354
  if (activeLineId || isAddNoteOpen) {
337
355
  sheetRef.current?.dismiss();
338
- } else {
356
+ return;
357
+ }
358
+ const t = setTimeout(() => {
339
359
  sheetRef.current?.present();
360
+ sheetRef.current?.snapToIndex(snapPointsArr.length - 1);
340
361
  setMinimized(false);
341
- }
342
- }, [activeLineId, isAddNoteOpen]);
362
+ }, 80);
363
+ return () => clearTimeout(t);
364
+ }, [activeLineId, isAddNoteOpen, snapPointsArr.length]);
343
365
  const handleSheetChange = (index) => {
344
366
  setMinimized(index === 0);
345
367
  };
@@ -357,6 +379,7 @@ function ProjectSheet({ snapPoints = ["35%", "85%"], renderHeader, children }) {
357
379
  snapPoints: snapPointsArr,
358
380
  enablePanDownToClose: false,
359
381
  enableDismissOnClose: false,
382
+ enableOverDrag: false,
360
383
  onChange: handleSheetChange,
361
384
  backdropComponent: (props) => /* @__PURE__ */ React5.createElement(
362
385
  BottomSheetBackdrop,
@@ -447,25 +470,29 @@ import React6, { useCallback as useCallback2, useEffect as useEffect3, useMemo a
447
470
  import { Pressable as Pressable2, StyleSheet as StyleSheet5, Text as Text2, View as View5 } from "react-native";
448
471
  import {
449
472
  BottomSheetBackdrop as BottomSheetBackdrop2,
450
- BottomSheetFlatList,
451
473
  BottomSheetFooter,
452
- BottomSheetModal as BottomSheetModal2
474
+ BottomSheetModal as BottomSheetModal2,
475
+ BottomSheetScrollView
453
476
  } from "@gorhom/bottom-sheet";
454
477
  import Svg, { Circle } from "react-native-svg";
455
478
  import { ArrowDown as ArrowDown2, ArrowUp as ArrowUp2, Pause, Play, Plus } from "lucide-react-native";
456
- function LineDetailSheet({ snapPoints = ["35%", "85%"], renderHeader, children }) {
479
+ function LineDetailSheet({ snapPoints = ["35%", "75%"], renderHeader, children }) {
457
480
  const { activeLine, isAddNoteOpen, controls } = useRetorBridge();
458
481
  const sheetRef = useRef3(null);
459
482
  const snapPointsArr = useMemo4(() => snapPoints, [snapPoints]);
460
483
  const [minimized, setMinimized] = useState3(false);
461
484
  useEffect3(() => {
462
- if (activeLine && !isAddNoteOpen) {
463
- sheetRef.current?.present();
464
- setMinimized(false);
465
- } else {
485
+ if (!activeLine || isAddNoteOpen) {
466
486
  sheetRef.current?.dismiss();
487
+ return;
467
488
  }
468
- }, [activeLine, isAddNoteOpen]);
489
+ const t = setTimeout(() => {
490
+ sheetRef.current?.present();
491
+ sheetRef.current?.snapToIndex(snapPointsArr.length - 1);
492
+ setMinimized(false);
493
+ }, 80);
494
+ return () => clearTimeout(t);
495
+ }, [activeLine, isAddNoteOpen, snapPointsArr.length]);
469
496
  const handleSheetChange = (index) => {
470
497
  setMinimized(index === 0);
471
498
  };
@@ -488,6 +515,7 @@ function LineDetailSheet({ snapPoints = ["35%", "85%"], renderHeader, children }
488
515
  snapPoints: snapPointsArr,
489
516
  enablePanDownToClose: false,
490
517
  enableDismissOnClose: false,
518
+ enableOverDrag: false,
491
519
  onChange: handleSheetChange,
492
520
  footerComponent: renderFooter,
493
521
  backdropComponent: (props) => /* @__PURE__ */ React6.createElement(
@@ -535,45 +563,37 @@ function AutoplayButton() {
535
563
  }
536
564
  function LineTagList({ children, listHeader }) {
537
565
  const { activeLine, closestTagId } = useRetorBridge();
538
- const listRef = useRef3(null);
566
+ const scrollRef = useRef3(null);
567
+ const offsetsRef = useRef3(/* @__PURE__ */ new Map());
539
568
  const tags = useMemo4(
540
569
  () => (activeLine?.tags ?? []).filter((t) => t.name && t.name.trim().length > 0),
541
570
  [activeLine]
542
571
  );
572
+ useEffect3(() => {
573
+ offsetsRef.current = /* @__PURE__ */ new Map();
574
+ }, [activeLine?._id]);
543
575
  useEffect3(() => {
544
576
  if (!closestTagId) return;
545
- const index = tags.findIndex((t2) => t2._id === closestTagId);
546
- if (index < 0) return;
547
577
  const t = setTimeout(() => {
548
- try {
549
- listRef.current?.scrollToIndex({ index, animated: true, viewPosition: 0 });
550
- } catch {
578
+ const y = offsetsRef.current.get(closestTagId);
579
+ if (y != null) {
580
+ scrollRef.current?.scrollTo({ y, animated: true });
551
581
  }
552
- }, 50);
582
+ }, 60);
553
583
  return () => clearTimeout(t);
554
- }, [closestTagId, tags]);
584
+ }, [closestTagId]);
555
585
  if (!activeLine) return null;
586
+ const handleItemLayout = (id, e) => {
587
+ offsetsRef.current.set(id, e.nativeEvent.layout.y);
588
+ };
556
589
  return /* @__PURE__ */ React6.createElement(
557
- BottomSheetFlatList,
590
+ BottomSheetScrollView,
558
591
  {
559
- ref: listRef,
560
- data: tags,
561
- keyExtractor: (t) => t._id,
562
- ListHeaderComponent: listHeader ? () => /* @__PURE__ */ React6.createElement(React6.Fragment, null, listHeader) : void 0,
563
- renderItem: ({ item }) => /* @__PURE__ */ React6.createElement(View5, null, children(item, item._id === closestTagId)),
564
- contentContainerStyle: styles3.list,
565
- removeClippedSubviews: false,
566
- onScrollToIndexFailed: (info) => {
567
- const offset = info.averageItemLength * info.index;
568
- listRef.current?.scrollToOffset({ offset, animated: false });
569
- setTimeout(() => {
570
- try {
571
- listRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0 });
572
- } catch {
573
- }
574
- }, 100);
575
- }
576
- }
592
+ ref: scrollRef,
593
+ contentContainerStyle: styles3.list
594
+ },
595
+ listHeader,
596
+ tags.map((tag) => /* @__PURE__ */ React6.createElement(View5, { key: tag._id, onLayout: (e) => handleItemLayout(tag._id, e) }, children(tag, tag._id === closestTagId)))
577
597
  );
578
598
  }
579
599
  function DefaultLineTagList({ listHeader }) {
@@ -625,10 +645,11 @@ var styles3 = StyleSheet5.create({
625
645
  justifyContent: "center",
626
646
  position: "relative"
627
647
  },
628
- list: { paddingHorizontal: 16, paddingBottom: 96, gap: 4 },
648
+ list: { paddingBottom: 96, gap: 4 },
629
649
  tagItem: {
630
650
  flexDirection: "row",
631
651
  alignItems: "center",
652
+ marginHorizontal: 16,
632
653
  paddingHorizontal: 12,
633
654
  paddingVertical: 12,
634
655
  borderRadius: 12,
@@ -718,6 +739,7 @@ function AddNoteSheet({
718
739
  ref: sheetRef,
719
740
  snapPoints: snapPointsArr,
720
741
  enablePanDownToClose: true,
742
+ enableOverDrag: false,
721
743
  onDismiss: closeAddNote,
722
744
  backdropComponent: (props) => /* @__PURE__ */ React7.createElement(
723
745
  BottomSheetBackdrop3,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retor/react-native",
3
- "version": "0.3.1",
3
+ "version": "0.3.4",
4
4
  "description": "React Native SDK for embedding Retor 3D experiences",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",