@retor/react-native 0.3.4 → 0.4.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/dist/index.d.mts CHANGED
@@ -43,6 +43,15 @@ interface InitPayload {
43
43
  interface LineProgressPayload {
44
44
  progress: number;
45
45
  closestTagId: string | null;
46
+ /** Point on the line at current progress (project-local space). */
47
+ targetPosition?: {
48
+ x: number;
49
+ y: number;
50
+ z: number;
51
+ } | null;
52
+ /** Distance along the line from the start.
53
+ * In meters if GPS is configured on the project, else project-local units. */
54
+ distanceFromStart?: number | null;
46
55
  }
47
56
  interface ViewerHandle {
48
57
  openLine: (lineId: string) => void;
@@ -55,29 +64,39 @@ interface ViewerHandle {
55
64
  interface NoteSubmitPayload {
56
65
  text: string;
57
66
  isPrivate: boolean;
67
+ /** The tag that was active when the + button was pressed. */
58
68
  tagId: string | null;
59
69
  lineId: string | null;
70
+ /**
71
+ * Point on the line at the scroll position when the + button was pressed,
72
+ * in project-local space. This is where the note visually anchors.
73
+ */
60
74
  position: {
61
75
  x: number;
62
76
  y: number;
63
77
  z: number;
64
78
  } | null;
79
+ /** Scroll progress along the line (0..1) when the + was pressed. */
80
+ progress: number;
81
+ /** Distance from the start of the line when the + was pressed.
82
+ * In meters if GPS is configured on the project, else project-local units. */
83
+ distanceFromStart: number | null;
65
84
  }
85
+ /**
86
+ * Stable data + commands. Updates only when meaningful things change
87
+ * (line opens/closes, notes loaded, project changes). Does NOT update per frame.
88
+ */
66
89
  interface RetorBridgeContextValue {
67
90
  project: RetorProject | null;
68
91
  lines: RetorLine[];
69
92
  activeLineId: string | null;
70
93
  activeLine: RetorLine | null;
71
- closestTagId: string | null;
72
- progress: number;
73
- isPlaying: boolean;
74
94
  isAddNoteOpen: boolean;
75
95
  addNoteTagId: string | null;
76
96
  controls: ViewerHandle;
77
97
  openAddNote: (tagId?: string) => void;
78
98
  closeAddNote: () => void;
79
99
  submitNote: (text: string, isPrivate?: boolean) => void;
80
- onNoteSubmit?: (payload: NoteSubmitPayload) => void;
81
100
  }
82
101
  declare function useRetorBridge(): RetorBridgeContextValue;
83
102
  /** Returns the array of all lines in the current project. */
@@ -91,11 +110,18 @@ declare function useProject(): RetorProject | null;
91
110
  declare function useActiveLine(): RetorLine | null;
92
111
  /**
93
112
  * Returns scroll progress (0..1) and the id of the closest tag along the
94
- * currently active line. Both are null when no line is open.
113
+ * currently active line. Components using this re-render on every frame
114
+ * during camera scroll — only call from leaves (tag items, progress rings).
95
115
  */
96
116
  declare function useLineProgress(): {
97
117
  progress: number;
98
118
  closestTagId: string | null;
119
+ targetPosition: {
120
+ x: number;
121
+ y: number;
122
+ z: number;
123
+ } | null;
124
+ distanceFromStart: number | null;
99
125
  };
100
126
  /**
101
127
  * Returns the autoplay state and controls.
@@ -165,16 +191,26 @@ declare const Viewer: React.ForwardRefExoticComponent<ViewerProps & React.RefAtt
165
191
 
166
192
  interface HudProps {
167
193
  children: React.ReactNode;
194
+ /**
195
+ * Maximum top inset for all sheets (in pixels). The sheets won't expand
196
+ * past `screenHeight - topInset`. Use this to leave room for your app's
197
+ * floating header buttons. Defaults to 0 (sheets can extend to the very top).
198
+ *
199
+ * Per-sheet `topInset` props override this default.
200
+ */
201
+ topInset?: number;
168
202
  }
169
203
  /**
170
204
  * Sets up the bottom-sheet provider tree for the SDK's UI components
171
205
  * (`<ProjectSheet>`, `<LineDetailSheet>`, `<AddNoteSheet>`).
172
206
  *
173
- * Place inside `<Viewer>` so the sheets can read scene state from the bridge:
207
+ * Place inside `<Viewer>` so the sheets can read scene state from the bridge.
208
+ * Pass `topInset` to set a global ceiling so the sheets won't overlap your
209
+ * app's floating header buttons.
174
210
  *
175
211
  * ```tsx
176
212
  * <Viewer projectId="...">
177
- * <Hud>
213
+ * <Hud topInset={160}>
178
214
  * <ProjectSheet />
179
215
  * <LineDetailSheet />
180
216
  * <AddNoteSheet />
@@ -182,11 +218,16 @@ interface HudProps {
182
218
  * </Viewer>
183
219
  * ```
184
220
  */
185
- declare function Hud({ children }: HudProps): React.JSX.Element;
221
+ declare function Hud({ children, topInset }: HudProps): React.JSX.Element;
186
222
 
187
223
  interface ProjectSheetProps {
188
224
  /** Snap points. Defaults to ["35%", "75%"]. */
189
225
  snapPoints?: (string | number)[];
226
+ /**
227
+ * Top inset in pixels — the sheet won't expand past `screenHeight - topInset`.
228
+ * Overrides the default set on `<Hud>`.
229
+ */
230
+ topInset?: number;
190
231
  /** Override the default header (project name + description + arrow button). */
191
232
  renderHeader?: (project: {
192
233
  name?: string;
@@ -198,7 +239,7 @@ interface ProjectSheetProps {
198
239
  /**
199
240
  * Browse-mode bottom sheet — auto-presents whenever no line is open.
200
241
  */
201
- declare function ProjectSheet({ snapPoints, renderHeader, children }: ProjectSheetProps): React.JSX.Element;
242
+ declare function ProjectSheet({ snapPoints, topInset, renderHeader, children }: ProjectSheetProps): React.JSX.Element;
202
243
  interface LinesCarouselProps {
203
244
  children: (line: RetorLine, index: number) => React.ReactNode;
204
245
  gap?: number;
@@ -209,12 +250,17 @@ declare function LinesCarousel({ children, gap, paddingHorizontal }: LinesCarous
209
250
  interface LineDetailSheetProps {
210
251
  /** Snap points. Defaults to ["35%", "75%"]. */
211
252
  snapPoints?: (string | number)[];
253
+ /**
254
+ * Top inset in pixels — the sheet won't expand past `screenHeight - topInset`.
255
+ * Overrides the default set on `<Hud>`.
256
+ */
257
+ topInset?: number;
212
258
  /** Override the header. */
213
259
  renderHeader?: (line: RetorLine) => React.ReactNode;
214
260
  /** Custom content (typically a `<LineTagList>`). */
215
261
  children?: React.ReactNode;
216
262
  }
217
- declare function LineDetailSheet({ snapPoints, renderHeader, children }: LineDetailSheetProps): React.JSX.Element;
263
+ declare function LineDetailSheet({ snapPoints, topInset, renderHeader, children }: LineDetailSheetProps): React.JSX.Element;
218
264
  interface LineTagListProps {
219
265
  children: (tag: RetorTag, isActive: boolean) => React.ReactNode;
220
266
  /** Optional header rendered above the list (used internally for the default header). */
@@ -231,6 +277,11 @@ declare function LineTagList({ children, listHeader }: LineTagListProps): React.
231
277
  interface AddNoteSheetProps {
232
278
  /** Snap points. Defaults to ["50%"]. */
233
279
  snapPoints?: (string | number)[];
280
+ /**
281
+ * Top inset in pixels — the sheet won't expand past `screenHeight - topInset`.
282
+ * Overrides the default set on `<Hud>`.
283
+ */
284
+ topInset?: number;
234
285
  /** Max characters allowed. Defaults to 280. */
235
286
  maxLength?: number;
236
287
  /** Placeholder text. */
@@ -251,7 +302,7 @@ interface AddNoteFormApi {
251
302
  * Sheet for adding a note. Auto-presents when `useAddNote().open()` is called.
252
303
  * On submit, fires `onNoteSubmit` on the parent `<Viewer>` with the text + position.
253
304
  */
254
- declare function AddNoteSheet({ snapPoints, maxLength, placeholder, renderForm, }: AddNoteSheetProps): React.JSX.Element;
305
+ declare function AddNoteSheet({ snapPoints, topInset, maxLength, placeholder, renderForm, }: AddNoteSheetProps): React.JSX.Element;
255
306
 
256
307
  interface CoverPhotoProps {
257
308
  projectId: string;
package/dist/index.d.ts CHANGED
@@ -43,6 +43,15 @@ interface InitPayload {
43
43
  interface LineProgressPayload {
44
44
  progress: number;
45
45
  closestTagId: string | null;
46
+ /** Point on the line at current progress (project-local space). */
47
+ targetPosition?: {
48
+ x: number;
49
+ y: number;
50
+ z: number;
51
+ } | null;
52
+ /** Distance along the line from the start.
53
+ * In meters if GPS is configured on the project, else project-local units. */
54
+ distanceFromStart?: number | null;
46
55
  }
47
56
  interface ViewerHandle {
48
57
  openLine: (lineId: string) => void;
@@ -55,29 +64,39 @@ interface ViewerHandle {
55
64
  interface NoteSubmitPayload {
56
65
  text: string;
57
66
  isPrivate: boolean;
67
+ /** The tag that was active when the + button was pressed. */
58
68
  tagId: string | null;
59
69
  lineId: string | null;
70
+ /**
71
+ * Point on the line at the scroll position when the + button was pressed,
72
+ * in project-local space. This is where the note visually anchors.
73
+ */
60
74
  position: {
61
75
  x: number;
62
76
  y: number;
63
77
  z: number;
64
78
  } | null;
79
+ /** Scroll progress along the line (0..1) when the + was pressed. */
80
+ progress: number;
81
+ /** Distance from the start of the line when the + was pressed.
82
+ * In meters if GPS is configured on the project, else project-local units. */
83
+ distanceFromStart: number | null;
65
84
  }
85
+ /**
86
+ * Stable data + commands. Updates only when meaningful things change
87
+ * (line opens/closes, notes loaded, project changes). Does NOT update per frame.
88
+ */
66
89
  interface RetorBridgeContextValue {
67
90
  project: RetorProject | null;
68
91
  lines: RetorLine[];
69
92
  activeLineId: string | null;
70
93
  activeLine: RetorLine | null;
71
- closestTagId: string | null;
72
- progress: number;
73
- isPlaying: boolean;
74
94
  isAddNoteOpen: boolean;
75
95
  addNoteTagId: string | null;
76
96
  controls: ViewerHandle;
77
97
  openAddNote: (tagId?: string) => void;
78
98
  closeAddNote: () => void;
79
99
  submitNote: (text: string, isPrivate?: boolean) => void;
80
- onNoteSubmit?: (payload: NoteSubmitPayload) => void;
81
100
  }
82
101
  declare function useRetorBridge(): RetorBridgeContextValue;
83
102
  /** Returns the array of all lines in the current project. */
@@ -91,11 +110,18 @@ declare function useProject(): RetorProject | null;
91
110
  declare function useActiveLine(): RetorLine | null;
92
111
  /**
93
112
  * Returns scroll progress (0..1) and the id of the closest tag along the
94
- * currently active line. Both are null when no line is open.
113
+ * currently active line. Components using this re-render on every frame
114
+ * during camera scroll — only call from leaves (tag items, progress rings).
95
115
  */
96
116
  declare function useLineProgress(): {
97
117
  progress: number;
98
118
  closestTagId: string | null;
119
+ targetPosition: {
120
+ x: number;
121
+ y: number;
122
+ z: number;
123
+ } | null;
124
+ distanceFromStart: number | null;
99
125
  };
100
126
  /**
101
127
  * Returns the autoplay state and controls.
@@ -165,16 +191,26 @@ declare const Viewer: React.ForwardRefExoticComponent<ViewerProps & React.RefAtt
165
191
 
166
192
  interface HudProps {
167
193
  children: React.ReactNode;
194
+ /**
195
+ * Maximum top inset for all sheets (in pixels). The sheets won't expand
196
+ * past `screenHeight - topInset`. Use this to leave room for your app's
197
+ * floating header buttons. Defaults to 0 (sheets can extend to the very top).
198
+ *
199
+ * Per-sheet `topInset` props override this default.
200
+ */
201
+ topInset?: number;
168
202
  }
169
203
  /**
170
204
  * Sets up the bottom-sheet provider tree for the SDK's UI components
171
205
  * (`<ProjectSheet>`, `<LineDetailSheet>`, `<AddNoteSheet>`).
172
206
  *
173
- * Place inside `<Viewer>` so the sheets can read scene state from the bridge:
207
+ * Place inside `<Viewer>` so the sheets can read scene state from the bridge.
208
+ * Pass `topInset` to set a global ceiling so the sheets won't overlap your
209
+ * app's floating header buttons.
174
210
  *
175
211
  * ```tsx
176
212
  * <Viewer projectId="...">
177
- * <Hud>
213
+ * <Hud topInset={160}>
178
214
  * <ProjectSheet />
179
215
  * <LineDetailSheet />
180
216
  * <AddNoteSheet />
@@ -182,11 +218,16 @@ interface HudProps {
182
218
  * </Viewer>
183
219
  * ```
184
220
  */
185
- declare function Hud({ children }: HudProps): React.JSX.Element;
221
+ declare function Hud({ children, topInset }: HudProps): React.JSX.Element;
186
222
 
187
223
  interface ProjectSheetProps {
188
224
  /** Snap points. Defaults to ["35%", "75%"]. */
189
225
  snapPoints?: (string | number)[];
226
+ /**
227
+ * Top inset in pixels — the sheet won't expand past `screenHeight - topInset`.
228
+ * Overrides the default set on `<Hud>`.
229
+ */
230
+ topInset?: number;
190
231
  /** Override the default header (project name + description + arrow button). */
191
232
  renderHeader?: (project: {
192
233
  name?: string;
@@ -198,7 +239,7 @@ interface ProjectSheetProps {
198
239
  /**
199
240
  * Browse-mode bottom sheet — auto-presents whenever no line is open.
200
241
  */
201
- declare function ProjectSheet({ snapPoints, renderHeader, children }: ProjectSheetProps): React.JSX.Element;
242
+ declare function ProjectSheet({ snapPoints, topInset, renderHeader, children }: ProjectSheetProps): React.JSX.Element;
202
243
  interface LinesCarouselProps {
203
244
  children: (line: RetorLine, index: number) => React.ReactNode;
204
245
  gap?: number;
@@ -209,12 +250,17 @@ declare function LinesCarousel({ children, gap, paddingHorizontal }: LinesCarous
209
250
  interface LineDetailSheetProps {
210
251
  /** Snap points. Defaults to ["35%", "75%"]. */
211
252
  snapPoints?: (string | number)[];
253
+ /**
254
+ * Top inset in pixels — the sheet won't expand past `screenHeight - topInset`.
255
+ * Overrides the default set on `<Hud>`.
256
+ */
257
+ topInset?: number;
212
258
  /** Override the header. */
213
259
  renderHeader?: (line: RetorLine) => React.ReactNode;
214
260
  /** Custom content (typically a `<LineTagList>`). */
215
261
  children?: React.ReactNode;
216
262
  }
217
- declare function LineDetailSheet({ snapPoints, renderHeader, children }: LineDetailSheetProps): React.JSX.Element;
263
+ declare function LineDetailSheet({ snapPoints, topInset, renderHeader, children }: LineDetailSheetProps): React.JSX.Element;
218
264
  interface LineTagListProps {
219
265
  children: (tag: RetorTag, isActive: boolean) => React.ReactNode;
220
266
  /** Optional header rendered above the list (used internally for the default header). */
@@ -231,6 +277,11 @@ declare function LineTagList({ children, listHeader }: LineTagListProps): React.
231
277
  interface AddNoteSheetProps {
232
278
  /** Snap points. Defaults to ["50%"]. */
233
279
  snapPoints?: (string | number)[];
280
+ /**
281
+ * Top inset in pixels — the sheet won't expand past `screenHeight - topInset`.
282
+ * Overrides the default set on `<Hud>`.
283
+ */
284
+ topInset?: number;
234
285
  /** Max characters allowed. Defaults to 280. */
235
286
  maxLength?: number;
236
287
  /** Placeholder text. */
@@ -251,7 +302,7 @@ interface AddNoteFormApi {
251
302
  * Sheet for adding a note. Auto-presents when `useAddNote().open()` is called.
252
303
  * On submit, fires `onNoteSubmit` on the parent `<Viewer>` with the text + position.
253
304
  */
254
- declare function AddNoteSheet({ snapPoints, maxLength, placeholder, renderForm, }: AddNoteSheetProps): React.JSX.Element;
305
+ declare function AddNoteSheet({ snapPoints, topInset, maxLength, placeholder, renderForm, }: AddNoteSheetProps): React.JSX.Element;
255
306
 
256
307
  interface CoverPhotoProps {
257
308
  projectId: string;
package/dist/index.js CHANGED
@@ -54,6 +54,13 @@ module.exports = __toCommonJS(index_exports);
54
54
  // src/context.tsx
55
55
  var import_react = __toESM(require("react"));
56
56
  var RetorBridgeContext = (0, import_react.createContext)(null);
57
+ var RetorProgressContext = (0, import_react.createContext)({
58
+ progress: 0,
59
+ closestTagId: null,
60
+ targetPosition: null,
61
+ distanceFromStart: null,
62
+ isPlaying: false
63
+ });
57
64
  function noop() {
58
65
  }
59
66
  var noopHandle = {
@@ -68,9 +75,6 @@ var fallback = {
68
75
  lines: [],
69
76
  activeLineId: null,
70
77
  activeLine: null,
71
- closestTagId: null,
72
- progress: 0,
73
- isPlaying: false,
74
78
  isAddNoteOpen: false,
75
79
  addNoteTagId: null,
76
80
  controls: noopHandle,
@@ -81,6 +85,9 @@ var fallback = {
81
85
  function useRetorBridge() {
82
86
  return (0, import_react.useContext)(RetorBridgeContext) ?? fallback;
83
87
  }
88
+ function useRetorProgress() {
89
+ return (0, import_react.useContext)(RetorProgressContext);
90
+ }
84
91
  function useLines() {
85
92
  return useRetorBridge().lines;
86
93
  }
@@ -91,11 +98,15 @@ function useActiveLine() {
91
98
  return useRetorBridge().activeLine;
92
99
  }
93
100
  function useLineProgress() {
94
- const { progress, closestTagId } = useRetorBridge();
95
- return (0, import_react.useMemo)(() => ({ progress, closestTagId }), [progress, closestTagId]);
101
+ const { progress, closestTagId, targetPosition, distanceFromStart } = useRetorProgress();
102
+ return (0, import_react.useMemo)(
103
+ () => ({ progress, closestTagId, targetPosition, distanceFromStart }),
104
+ [progress, closestTagId, targetPosition, distanceFromStart]
105
+ );
96
106
  }
97
107
  function useAutoplay() {
98
- const { isPlaying, controls } = useRetorBridge();
108
+ const { isPlaying } = useRetorProgress();
109
+ const { controls } = useRetorBridge();
99
110
  return (0, import_react.useMemo)(() => ({
100
111
  isPlaying,
101
112
  toggle: () => controls.toggleAutoplay(),
@@ -116,6 +127,9 @@ function useAddNote() {
116
127
  function RetorBridgeProvider({ value, children }) {
117
128
  return /* @__PURE__ */ import_react.default.createElement(RetorBridgeContext.Provider, { value }, children);
118
129
  }
130
+ function RetorProgressProvider({ value, children }) {
131
+ return /* @__PURE__ */ import_react.default.createElement(RetorProgressContext.Provider, { value }, children);
132
+ }
119
133
 
120
134
  // src/Viewer.tsx
121
135
  var import_react2 = __toESM(require("react"));
@@ -194,6 +208,8 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
194
208
  const [activeLineId, setActiveLineId] = (0, import_react2.useState)(null);
195
209
  const [closestTagId, setClosestTagId] = (0, import_react2.useState)(null);
196
210
  const [progress, setProgress] = (0, import_react2.useState)(0);
211
+ const [targetPosition, setTargetPosition] = (0, import_react2.useState)(null);
212
+ const [distanceFromStart, setDistanceFromStart] = (0, import_react2.useState)(null);
197
213
  const [isPlaying, setIsPlaying] = (0, import_react2.useState)(false);
198
214
  const [isAddNoteOpen, setIsAddNoteOpen] = (0, import_react2.useState)(false);
199
215
  const [addNoteTagId, setAddNoteTagId] = (0, import_react2.useState)(null);
@@ -235,8 +251,11 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
235
251
  const closestTagIdRef = (0, import_react2.useRef)(closestTagId);
236
252
  const addNoteTagIdRef = (0, import_react2.useRef)(addNoteTagId);
237
253
  const activeLineIdRef = (0, import_react2.useRef)(activeLineId);
238
- const linesRef = (0, import_react2.useRef)(lines);
239
254
  const onNoteSubmitRef = (0, import_react2.useRef)(onNoteSubmit);
255
+ const noteSnapshotRef = (0, import_react2.useRef)({ progress: 0, targetPosition: null, distanceFromStart: null });
256
+ const progressRef = (0, import_react2.useRef)(progress);
257
+ const targetPositionRef = (0, import_react2.useRef)(targetPosition);
258
+ const distanceFromStartRef = (0, import_react2.useRef)(distanceFromStart);
240
259
  (0, import_react2.useEffect)(() => {
241
260
  closestTagIdRef.current = closestTagId;
242
261
  }, [closestTagId]);
@@ -246,14 +265,25 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
246
265
  (0, import_react2.useEffect)(() => {
247
266
  activeLineIdRef.current = activeLineId;
248
267
  }, [activeLineId]);
249
- (0, import_react2.useEffect)(() => {
250
- linesRef.current = lines;
251
- }, [lines]);
252
268
  (0, import_react2.useEffect)(() => {
253
269
  onNoteSubmitRef.current = onNoteSubmit;
254
270
  }, [onNoteSubmit]);
271
+ (0, import_react2.useEffect)(() => {
272
+ progressRef.current = progress;
273
+ }, [progress]);
274
+ (0, import_react2.useEffect)(() => {
275
+ targetPositionRef.current = targetPosition;
276
+ }, [targetPosition]);
277
+ (0, import_react2.useEffect)(() => {
278
+ distanceFromStartRef.current = distanceFromStart;
279
+ }, [distanceFromStart]);
255
280
  const openAddNote = (0, import_react2.useCallback)((tagId) => {
256
281
  setAddNoteTagId(tagId ?? closestTagIdRef.current ?? null);
282
+ noteSnapshotRef.current = {
283
+ progress: progressRef.current,
284
+ targetPosition: targetPositionRef.current,
285
+ distanceFromStart: distanceFromStartRef.current
286
+ };
257
287
  setIsAddNoteOpen(true);
258
288
  }, []);
259
289
  const closeAddNote = (0, import_react2.useCallback)(() => {
@@ -262,13 +292,15 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
262
292
  const submitNote = (0, import_react2.useCallback)((text, isPrivate = true) => {
263
293
  const tagId = addNoteTagIdRef.current;
264
294
  const lineId = activeLineIdRef.current;
265
- const tag = linesRef.current.find((l) => l._id === lineId)?.tags.find((t) => t._id === tagId);
295
+ const snap = noteSnapshotRef.current;
266
296
  const payload = {
267
297
  text,
268
298
  isPrivate,
269
299
  tagId,
270
300
  lineId,
271
- position: tag?.position ?? null
301
+ position: snap.targetPosition,
302
+ progress: snap.progress,
303
+ distanceFromStart: snap.distanceFromStart
272
304
  };
273
305
  onNoteSubmitRef.current?.(payload);
274
306
  setIsAddNoteOpen(false);
@@ -309,6 +341,8 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
309
341
  const payload = data.payload;
310
342
  setProgress(payload.progress);
311
343
  setClosestTagId(payload.closestTagId);
344
+ setTargetPosition(payload.targetPosition ?? null);
345
+ setDistanceFromStart(payload.distanceFromStart ?? null);
312
346
  onLineProgress?.(payload);
313
347
  break;
314
348
  }
@@ -333,20 +367,20 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
333
367
  lines,
334
368
  activeLineId,
335
369
  activeLine,
336
- closestTagId,
337
- progress,
338
- isPlaying,
339
370
  isAddNoteOpen,
340
371
  addNoteTagId,
341
372
  controls,
342
373
  openAddNote,
343
374
  closeAddNote,
344
- submitNote,
345
- onNoteSubmit
375
+ submitNote
346
376
  }),
347
- [project, lines, activeLineId, activeLine, closestTagId, progress, isPlaying, isAddNoteOpen, addNoteTagId, controls, openAddNote, closeAddNote, submitNote, onNoteSubmit]
377
+ [project, lines, activeLineId, activeLine, isAddNoteOpen, addNoteTagId, controls, openAddNote, closeAddNote, submitNote]
378
+ );
379
+ const progressCtx = (0, import_react2.useMemo)(
380
+ () => ({ progress, closestTagId, targetPosition, distanceFromStart, isPlaying }),
381
+ [progress, closestTagId, targetPosition, distanceFromStart, isPlaying]
348
382
  );
349
- return /* @__PURE__ */ import_react2.default.createElement(RetorBridgeProvider, { value: ctxValue }, /* @__PURE__ */ import_react2.default.createElement(import_react_native.View, { style: [styles.root, style] }, /* @__PURE__ */ import_react2.default.createElement(
383
+ return /* @__PURE__ */ import_react2.default.createElement(RetorBridgeProvider, { value: ctxValue }, /* @__PURE__ */ import_react2.default.createElement(RetorProgressProvider, { value: progressCtx }, /* @__PURE__ */ import_react2.default.createElement(import_react_native.View, { style: [styles.root, style] }, /* @__PURE__ */ import_react2.default.createElement(
350
384
  import_react_native_webview.WebView,
351
385
  {
352
386
  ref: webviewRef,
@@ -359,7 +393,7 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
359
393
  allowsInlineMediaPlayback: true,
360
394
  mediaPlaybackRequiresUserAction: false
361
395
  }
362
- ), children));
396
+ ), children)));
363
397
  });
364
398
  var styles = import_react_native.StyleSheet.create({
365
399
  root: { flex: 1, position: "relative" },
@@ -370,8 +404,12 @@ var styles = import_react_native.StyleSheet.create({
370
404
  var import_react3 = __toESM(require("react"));
371
405
  var import_react_native2 = require("react-native");
372
406
  var import_bottom_sheet = require("@gorhom/bottom-sheet");
373
- function Hud({ children }) {
374
- return /* @__PURE__ */ import_react3.default.createElement(import_react_native2.View, { pointerEvents: "box-none", style: import_react_native2.StyleSheet.absoluteFill }, /* @__PURE__ */ import_react3.default.createElement(import_bottom_sheet.BottomSheetModalProvider, null, children));
407
+ var HudContext = (0, import_react3.createContext)({ topInset: 0 });
408
+ function useHudContext() {
409
+ return (0, import_react3.useContext)(HudContext);
410
+ }
411
+ function Hud({ children, topInset = 0 }) {
412
+ return /* @__PURE__ */ import_react3.default.createElement(HudContext.Provider, { value: { topInset } }, /* @__PURE__ */ import_react3.default.createElement(import_react_native2.View, { pointerEvents: "box-none", style: import_react_native2.StyleSheet.absoluteFill }, /* @__PURE__ */ import_react3.default.createElement(import_bottom_sheet.BottomSheetModalProvider, null, children)));
375
413
  }
376
414
 
377
415
  // src/ProjectSheet.tsx
@@ -389,8 +427,20 @@ function BlurBackground({ style }) {
389
427
  }
390
428
 
391
429
  // src/ProjectSheet.tsx
392
- function ProjectSheet({ snapPoints = ["35%", "75%"], renderHeader, children }) {
430
+ var renderBackdrop = (props) => /* @__PURE__ */ import_react5.default.createElement(
431
+ import_bottom_sheet2.BottomSheetBackdrop,
432
+ {
433
+ ...props,
434
+ appearsOnIndex: 1,
435
+ disappearsOnIndex: 0,
436
+ opacity: 0.4,
437
+ pressBehavior: "collapse"
438
+ }
439
+ );
440
+ function ProjectSheet({ snapPoints = ["35%", "75%"], topInset, renderHeader, children }) {
393
441
  const { project, activeLineId, isAddNoteOpen } = useRetorBridge();
442
+ const { topInset: hudTopInset } = useHudContext();
443
+ const effectiveTopInset = topInset ?? hudTopInset;
394
444
  const sheetRef = (0, import_react5.useRef)(null);
395
445
  const snapPointsArr = (0, import_react5.useMemo)(() => snapPoints, [snapPoints]);
396
446
  const [minimized, setMinimized] = (0, import_react5.useState)(false);
@@ -425,18 +475,10 @@ function ProjectSheet({ snapPoints = ["35%", "75%"], renderHeader, children }) {
425
475
  enableDismissOnClose: false,
426
476
  enableOverDrag: false,
427
477
  onChange: handleSheetChange,
428
- backdropComponent: (props) => /* @__PURE__ */ import_react5.default.createElement(
429
- import_bottom_sheet2.BottomSheetBackdrop,
430
- {
431
- ...props,
432
- appearsOnIndex: 1,
433
- disappearsOnIndex: 0,
434
- opacity: 0.4,
435
- pressBehavior: "collapse"
436
- }
437
- ),
478
+ backdropComponent: renderBackdrop,
438
479
  handleIndicatorStyle: styles2.handle,
439
- backgroundComponent: BlurBackground
480
+ backgroundComponent: BlurBackground,
481
+ topInset: effectiveTopInset
440
482
  },
441
483
  /* @__PURE__ */ import_react5.default.createElement(import_bottom_sheet2.BottomSheetView, { style: styles2.content }, renderHeader ? renderHeader({ name: project?.name, description: project?.description }) : /* @__PURE__ */ import_react5.default.createElement(import_react_native4.View, { style: styles2.header }, /* @__PURE__ */ import_react5.default.createElement(import_react_native4.View, { style: { flex: 1, minWidth: 0 } }, project?.name && /* @__PURE__ */ import_react5.default.createElement(import_react_native4.Text, { style: styles2.title, numberOfLines: 1 }, project.name), project?.description && /* @__PURE__ */ import_react5.default.createElement(import_react_native4.Text, { style: styles2.subtitle, numberOfLines: 4 }, project.description)), /* @__PURE__ */ import_react5.default.createElement(import_react_native4.Pressable, { style: styles2.iconBtn, onPress: toggleMinimize }, minimized ? /* @__PURE__ */ import_react5.default.createElement(import_lucide_react_native.ArrowUp, { size: 14, color: "rgba(255,255,255,0.6)" }) : /* @__PURE__ */ import_react5.default.createElement(import_lucide_react_native.ArrowDown, { size: 14, color: "rgba(255,255,255,0.6)" }))), children ?? /* @__PURE__ */ import_react5.default.createElement(DefaultLinesCarousel, null))
442
484
  );
@@ -515,8 +557,20 @@ var import_react_native5 = require("react-native");
515
557
  var import_bottom_sheet3 = require("@gorhom/bottom-sheet");
516
558
  var import_react_native_svg = __toESM(require("react-native-svg"));
517
559
  var import_lucide_react_native2 = require("lucide-react-native");
518
- function LineDetailSheet({ snapPoints = ["35%", "75%"], renderHeader, children }) {
560
+ var renderBackdrop2 = (props) => /* @__PURE__ */ import_react6.default.createElement(
561
+ import_bottom_sheet3.BottomSheetBackdrop,
562
+ {
563
+ ...props,
564
+ appearsOnIndex: 1,
565
+ disappearsOnIndex: 0,
566
+ opacity: 0.4,
567
+ pressBehavior: "collapse"
568
+ }
569
+ );
570
+ function LineDetailSheet({ snapPoints = ["35%", "75%"], topInset, renderHeader, children }) {
519
571
  const { activeLine, isAddNoteOpen, controls } = useRetorBridge();
572
+ const { topInset: hudTopInset } = useHudContext();
573
+ const effectiveTopInset = topInset ?? hudTopInset;
520
574
  const sheetRef = (0, import_react6.useRef)(null);
521
575
  const snapPointsArr = (0, import_react6.useMemo)(() => snapPoints, [snapPoints]);
522
576
  const [minimized, setMinimized] = (0, import_react6.useState)(false);
@@ -557,18 +611,10 @@ function LineDetailSheet({ snapPoints = ["35%", "75%"], renderHeader, children }
557
611
  enableOverDrag: false,
558
612
  onChange: handleSheetChange,
559
613
  footerComponent: renderFooter,
560
- backdropComponent: (props) => /* @__PURE__ */ import_react6.default.createElement(
561
- import_bottom_sheet3.BottomSheetBackdrop,
562
- {
563
- ...props,
564
- appearsOnIndex: 1,
565
- disappearsOnIndex: 0,
566
- opacity: 0.4,
567
- pressBehavior: "collapse"
568
- }
569
- ),
614
+ backdropComponent: renderBackdrop2,
570
615
  handleIndicatorStyle: styles3.handle,
571
- backgroundComponent: BlurBackground
616
+ backgroundComponent: BlurBackground,
617
+ topInset: effectiveTopInset
572
618
  },
573
619
  activeLine && (children ?? /* @__PURE__ */ import_react6.default.createElement(DefaultLineTagList, { listHeader: header }))
574
620
  );
@@ -581,7 +627,8 @@ function DefaultHeader({
581
627
  return /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { style: styles3.header }, /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Text, { style: styles3.title, numberOfLines: 1 }, line.name), line.subtitle && /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Text, { style: styles3.subtitle, numberOfLines: 1 }, line.subtitle), line.description && /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Text, { style: styles3.description, numberOfLines: minimized ? 2 : 4 }, line.description)), /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { style: styles3.headerActions }, /* @__PURE__ */ import_react6.default.createElement(AutoplayButton, null), /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Pressable, { style: styles3.iconBtn, onPress: onToggleMinimize }, minimized ? /* @__PURE__ */ import_react6.default.createElement(import_lucide_react_native2.ArrowUp, { size: 14, color: "rgba(255,255,255,0.6)" }) : /* @__PURE__ */ import_react6.default.createElement(import_lucide_react_native2.ArrowDown, { size: 14, color: "rgba(255,255,255,0.6)" }))));
582
628
  }
583
629
  function AutoplayButton() {
584
- const { isPlaying, progress, controls } = useRetorBridge();
630
+ const { controls } = useRetorBridge();
631
+ const { isPlaying, progress } = useRetorProgress();
585
632
  const r = 12.5;
586
633
  const c = 2 * Math.PI * r;
587
634
  return /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Pressable, { style: styles3.iconBtn, onPress: () => controls.toggleAutoplay() }, /* @__PURE__ */ import_react6.default.createElement(import_react_native_svg.default, { width: 28, height: 28, style: import_react_native5.StyleSheet.absoluteFill }, /* @__PURE__ */ import_react6.default.createElement(
@@ -601,7 +648,8 @@ function AutoplayButton() {
601
648
  )), isPlaying ? /* @__PURE__ */ import_react6.default.createElement(import_lucide_react_native2.Pause, { size: 11, color: "white", fill: "white" }) : /* @__PURE__ */ import_react6.default.createElement(import_lucide_react_native2.Play, { size: 11, color: "white", fill: "white", style: { marginLeft: 1 } }));
602
649
  }
603
650
  function LineTagList({ children, listHeader }) {
604
- const { activeLine, closestTagId } = useRetorBridge();
651
+ const { activeLine } = useRetorBridge();
652
+ const { closestTagId } = useRetorProgress();
605
653
  const scrollRef = (0, import_react6.useRef)(null);
606
654
  const offsetsRef = (0, import_react6.useRef)(/* @__PURE__ */ new Map());
607
655
  const tags = (0, import_react6.useMemo)(
@@ -611,14 +659,16 @@ function LineTagList({ children, listHeader }) {
611
659
  (0, import_react6.useEffect)(() => {
612
660
  offsetsRef.current = /* @__PURE__ */ new Map();
613
661
  }, [activeLine?._id]);
662
+ const lastScrolledIdRef = (0, import_react6.useRef)(null);
614
663
  (0, import_react6.useEffect)(() => {
615
- if (!closestTagId) return;
664
+ if (!closestTagId || closestTagId === lastScrolledIdRef.current) return;
616
665
  const t = setTimeout(() => {
617
666
  const y = offsetsRef.current.get(closestTagId);
618
667
  if (y != null) {
619
- scrollRef.current?.scrollTo({ y, animated: true });
668
+ scrollRef.current?.scrollTo?.({ y, animated: true });
669
+ lastScrolledIdRef.current = closestTagId;
620
670
  }
621
- }, 60);
671
+ }, 300);
622
672
  return () => clearTimeout(t);
623
673
  }, [closestTagId]);
624
674
  if (!activeLine) return null;
@@ -727,13 +777,25 @@ var import_react7 = __toESM(require("react"));
727
777
  var import_react_native6 = require("react-native");
728
778
  var import_bottom_sheet4 = require("@gorhom/bottom-sheet");
729
779
  var import_lucide_react_native3 = require("lucide-react-native");
780
+ var renderBackdrop3 = (props) => /* @__PURE__ */ import_react7.default.createElement(
781
+ import_bottom_sheet4.BottomSheetBackdrop,
782
+ {
783
+ ...props,
784
+ appearsOnIndex: 0,
785
+ disappearsOnIndex: -1,
786
+ opacity: 0.6
787
+ }
788
+ );
730
789
  function AddNoteSheet({
731
790
  snapPoints = ["50%"],
791
+ topInset,
732
792
  maxLength = 280,
733
793
  placeholder = "Write a note...",
734
794
  renderForm
735
795
  }) {
736
796
  const { isAddNoteOpen, addNoteTagId, activeLine, closeAddNote, submitNote } = useRetorBridge();
797
+ const { topInset: hudTopInset } = useHudContext();
798
+ const effectiveTopInset = topInset ?? hudTopInset;
737
799
  const sheetRef = (0, import_react7.useRef)(null);
738
800
  const snapPointsArr = (0, import_react7.useMemo)(() => snapPoints, [snapPoints]);
739
801
  const [text, setText] = (0, import_react7.useState)("");
@@ -775,17 +837,10 @@ function AddNoteSheet({
775
837
  enablePanDownToClose: true,
776
838
  enableOverDrag: false,
777
839
  onDismiss: closeAddNote,
778
- backdropComponent: (props) => /* @__PURE__ */ import_react7.default.createElement(
779
- import_bottom_sheet4.BottomSheetBackdrop,
780
- {
781
- ...props,
782
- appearsOnIndex: 0,
783
- disappearsOnIndex: -1,
784
- opacity: 0.6
785
- }
786
- ),
840
+ backdropComponent: renderBackdrop3,
787
841
  handleIndicatorStyle: styles4.handle,
788
- backgroundComponent: BlurBackground
842
+ backgroundComponent: BlurBackground,
843
+ topInset: effectiveTopInset
789
844
  },
790
845
  /* @__PURE__ */ import_react7.default.createElement(import_bottom_sheet4.BottomSheetView, { style: styles4.content }, renderForm ? renderForm(formApi) : /* @__PURE__ */ import_react7.default.createElement(import_react_native6.View, { style: styles4.form }, /* @__PURE__ */ import_react7.default.createElement(import_react_native6.View, { style: styles4.headerRow }, /* @__PURE__ */ import_react7.default.createElement(import_react_native6.View, { style: { flex: 1 } }, activeLine && /* @__PURE__ */ import_react7.default.createElement(import_react_native6.Text, { style: styles4.title, numberOfLines: 1 }, activeLine.name), tag && /* @__PURE__ */ import_react7.default.createElement(import_react_native6.Text, { style: styles4.subtitle, numberOfLines: 1 }, tag.name)), /* @__PURE__ */ import_react7.default.createElement(import_react_native6.Pressable, { onPress: closeAddNote, style: styles4.closeBtn }, /* @__PURE__ */ import_react7.default.createElement(import_lucide_react_native3.X, { size: 14, color: "rgba(255,255,255,0.6)" }))), /* @__PURE__ */ import_react7.default.createElement(
791
846
  import_bottom_sheet4.BottomSheetTextInput,
package/dist/index.mjs CHANGED
@@ -1,6 +1,13 @@
1
1
  // src/context.tsx
2
2
  import React, { createContext, useContext, useMemo } from "react";
3
3
  var RetorBridgeContext = createContext(null);
4
+ var RetorProgressContext = createContext({
5
+ progress: 0,
6
+ closestTagId: null,
7
+ targetPosition: null,
8
+ distanceFromStart: null,
9
+ isPlaying: false
10
+ });
4
11
  function noop() {
5
12
  }
6
13
  var noopHandle = {
@@ -15,9 +22,6 @@ var fallback = {
15
22
  lines: [],
16
23
  activeLineId: null,
17
24
  activeLine: null,
18
- closestTagId: null,
19
- progress: 0,
20
- isPlaying: false,
21
25
  isAddNoteOpen: false,
22
26
  addNoteTagId: null,
23
27
  controls: noopHandle,
@@ -28,6 +32,9 @@ var fallback = {
28
32
  function useRetorBridge() {
29
33
  return useContext(RetorBridgeContext) ?? fallback;
30
34
  }
35
+ function useRetorProgress() {
36
+ return useContext(RetorProgressContext);
37
+ }
31
38
  function useLines() {
32
39
  return useRetorBridge().lines;
33
40
  }
@@ -38,11 +45,15 @@ function useActiveLine() {
38
45
  return useRetorBridge().activeLine;
39
46
  }
40
47
  function useLineProgress() {
41
- const { progress, closestTagId } = useRetorBridge();
42
- return useMemo(() => ({ progress, closestTagId }), [progress, closestTagId]);
48
+ const { progress, closestTagId, targetPosition, distanceFromStart } = useRetorProgress();
49
+ return useMemo(
50
+ () => ({ progress, closestTagId, targetPosition, distanceFromStart }),
51
+ [progress, closestTagId, targetPosition, distanceFromStart]
52
+ );
43
53
  }
44
54
  function useAutoplay() {
45
- const { isPlaying, controls } = useRetorBridge();
55
+ const { isPlaying } = useRetorProgress();
56
+ const { controls } = useRetorBridge();
46
57
  return useMemo(() => ({
47
58
  isPlaying,
48
59
  toggle: () => controls.toggleAutoplay(),
@@ -63,6 +74,9 @@ function useAddNote() {
63
74
  function RetorBridgeProvider({ value, children }) {
64
75
  return /* @__PURE__ */ React.createElement(RetorBridgeContext.Provider, { value }, children);
65
76
  }
77
+ function RetorProgressProvider({ value, children }) {
78
+ return /* @__PURE__ */ React.createElement(RetorProgressContext.Provider, { value }, children);
79
+ }
66
80
 
67
81
  // src/Viewer.tsx
68
82
  import React2, {
@@ -150,6 +164,8 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
150
164
  const [activeLineId, setActiveLineId] = useState(null);
151
165
  const [closestTagId, setClosestTagId] = useState(null);
152
166
  const [progress, setProgress] = useState(0);
167
+ const [targetPosition, setTargetPosition] = useState(null);
168
+ const [distanceFromStart, setDistanceFromStart] = useState(null);
153
169
  const [isPlaying, setIsPlaying] = useState(false);
154
170
  const [isAddNoteOpen, setIsAddNoteOpen] = useState(false);
155
171
  const [addNoteTagId, setAddNoteTagId] = useState(null);
@@ -191,8 +207,11 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
191
207
  const closestTagIdRef = useRef(closestTagId);
192
208
  const addNoteTagIdRef = useRef(addNoteTagId);
193
209
  const activeLineIdRef = useRef(activeLineId);
194
- const linesRef = useRef(lines);
195
210
  const onNoteSubmitRef = useRef(onNoteSubmit);
211
+ const noteSnapshotRef = useRef({ progress: 0, targetPosition: null, distanceFromStart: null });
212
+ const progressRef = useRef(progress);
213
+ const targetPositionRef = useRef(targetPosition);
214
+ const distanceFromStartRef = useRef(distanceFromStart);
196
215
  useEffect(() => {
197
216
  closestTagIdRef.current = closestTagId;
198
217
  }, [closestTagId]);
@@ -202,14 +221,25 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
202
221
  useEffect(() => {
203
222
  activeLineIdRef.current = activeLineId;
204
223
  }, [activeLineId]);
205
- useEffect(() => {
206
- linesRef.current = lines;
207
- }, [lines]);
208
224
  useEffect(() => {
209
225
  onNoteSubmitRef.current = onNoteSubmit;
210
226
  }, [onNoteSubmit]);
227
+ useEffect(() => {
228
+ progressRef.current = progress;
229
+ }, [progress]);
230
+ useEffect(() => {
231
+ targetPositionRef.current = targetPosition;
232
+ }, [targetPosition]);
233
+ useEffect(() => {
234
+ distanceFromStartRef.current = distanceFromStart;
235
+ }, [distanceFromStart]);
211
236
  const openAddNote = useCallback((tagId) => {
212
237
  setAddNoteTagId(tagId ?? closestTagIdRef.current ?? null);
238
+ noteSnapshotRef.current = {
239
+ progress: progressRef.current,
240
+ targetPosition: targetPositionRef.current,
241
+ distanceFromStart: distanceFromStartRef.current
242
+ };
213
243
  setIsAddNoteOpen(true);
214
244
  }, []);
215
245
  const closeAddNote = useCallback(() => {
@@ -218,13 +248,15 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
218
248
  const submitNote = useCallback((text, isPrivate = true) => {
219
249
  const tagId = addNoteTagIdRef.current;
220
250
  const lineId = activeLineIdRef.current;
221
- const tag = linesRef.current.find((l) => l._id === lineId)?.tags.find((t) => t._id === tagId);
251
+ const snap = noteSnapshotRef.current;
222
252
  const payload = {
223
253
  text,
224
254
  isPrivate,
225
255
  tagId,
226
256
  lineId,
227
- position: tag?.position ?? null
257
+ position: snap.targetPosition,
258
+ progress: snap.progress,
259
+ distanceFromStart: snap.distanceFromStart
228
260
  };
229
261
  onNoteSubmitRef.current?.(payload);
230
262
  setIsAddNoteOpen(false);
@@ -265,6 +297,8 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
265
297
  const payload = data.payload;
266
298
  setProgress(payload.progress);
267
299
  setClosestTagId(payload.closestTagId);
300
+ setTargetPosition(payload.targetPosition ?? null);
301
+ setDistanceFromStart(payload.distanceFromStart ?? null);
268
302
  onLineProgress?.(payload);
269
303
  break;
270
304
  }
@@ -289,20 +323,20 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
289
323
  lines,
290
324
  activeLineId,
291
325
  activeLine,
292
- closestTagId,
293
- progress,
294
- isPlaying,
295
326
  isAddNoteOpen,
296
327
  addNoteTagId,
297
328
  controls,
298
329
  openAddNote,
299
330
  closeAddNote,
300
- submitNote,
301
- onNoteSubmit
331
+ submitNote
302
332
  }),
303
- [project, lines, activeLineId, activeLine, closestTagId, progress, isPlaying, isAddNoteOpen, addNoteTagId, controls, openAddNote, closeAddNote, submitNote, onNoteSubmit]
333
+ [project, lines, activeLineId, activeLine, isAddNoteOpen, addNoteTagId, controls, openAddNote, closeAddNote, submitNote]
334
+ );
335
+ const progressCtx = useMemo2(
336
+ () => ({ progress, closestTagId, targetPosition, distanceFromStart, isPlaying }),
337
+ [progress, closestTagId, targetPosition, distanceFromStart, isPlaying]
304
338
  );
305
- return /* @__PURE__ */ React2.createElement(RetorBridgeProvider, { value: ctxValue }, /* @__PURE__ */ React2.createElement(View, { style: [styles.root, style] }, /* @__PURE__ */ React2.createElement(
339
+ return /* @__PURE__ */ React2.createElement(RetorBridgeProvider, { value: ctxValue }, /* @__PURE__ */ React2.createElement(RetorProgressProvider, { value: progressCtx }, /* @__PURE__ */ React2.createElement(View, { style: [styles.root, style] }, /* @__PURE__ */ React2.createElement(
306
340
  WebView,
307
341
  {
308
342
  ref: webviewRef,
@@ -315,7 +349,7 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
315
349
  allowsInlineMediaPlayback: true,
316
350
  mediaPlaybackRequiresUserAction: false
317
351
  }
318
- ), children));
352
+ ), children)));
319
353
  });
320
354
  var styles = StyleSheet.create({
321
355
  root: { flex: 1, position: "relative" },
@@ -323,11 +357,15 @@ var styles = StyleSheet.create({
323
357
  });
324
358
 
325
359
  // src/Hud.tsx
326
- import React3 from "react";
360
+ import React3, { createContext as createContext2, useContext as useContext2 } from "react";
327
361
  import { StyleSheet as StyleSheet2, View as View2 } from "react-native";
328
362
  import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
329
- function Hud({ children }) {
330
- return /* @__PURE__ */ React3.createElement(View2, { pointerEvents: "box-none", style: StyleSheet2.absoluteFill }, /* @__PURE__ */ React3.createElement(BottomSheetModalProvider, null, children));
363
+ var HudContext = createContext2({ topInset: 0 });
364
+ function useHudContext() {
365
+ return useContext2(HudContext);
366
+ }
367
+ function Hud({ children, topInset = 0 }) {
368
+ return /* @__PURE__ */ React3.createElement(HudContext.Provider, { value: { topInset } }, /* @__PURE__ */ React3.createElement(View2, { pointerEvents: "box-none", style: StyleSheet2.absoluteFill }, /* @__PURE__ */ React3.createElement(BottomSheetModalProvider, null, children)));
331
369
  }
332
370
 
333
371
  // src/ProjectSheet.tsx
@@ -345,8 +383,20 @@ function BlurBackground({ style }) {
345
383
  }
346
384
 
347
385
  // src/ProjectSheet.tsx
348
- function ProjectSheet({ snapPoints = ["35%", "75%"], renderHeader, children }) {
386
+ var renderBackdrop = (props) => /* @__PURE__ */ React5.createElement(
387
+ BottomSheetBackdrop,
388
+ {
389
+ ...props,
390
+ appearsOnIndex: 1,
391
+ disappearsOnIndex: 0,
392
+ opacity: 0.4,
393
+ pressBehavior: "collapse"
394
+ }
395
+ );
396
+ function ProjectSheet({ snapPoints = ["35%", "75%"], topInset, renderHeader, children }) {
349
397
  const { project, activeLineId, isAddNoteOpen } = useRetorBridge();
398
+ const { topInset: hudTopInset } = useHudContext();
399
+ const effectiveTopInset = topInset ?? hudTopInset;
350
400
  const sheetRef = useRef2(null);
351
401
  const snapPointsArr = useMemo3(() => snapPoints, [snapPoints]);
352
402
  const [minimized, setMinimized] = useState2(false);
@@ -381,18 +431,10 @@ function ProjectSheet({ snapPoints = ["35%", "75%"], renderHeader, children }) {
381
431
  enableDismissOnClose: false,
382
432
  enableOverDrag: false,
383
433
  onChange: handleSheetChange,
384
- backdropComponent: (props) => /* @__PURE__ */ React5.createElement(
385
- BottomSheetBackdrop,
386
- {
387
- ...props,
388
- appearsOnIndex: 1,
389
- disappearsOnIndex: 0,
390
- opacity: 0.4,
391
- pressBehavior: "collapse"
392
- }
393
- ),
434
+ backdropComponent: renderBackdrop,
394
435
  handleIndicatorStyle: styles2.handle,
395
- backgroundComponent: BlurBackground
436
+ backgroundComponent: BlurBackground,
437
+ topInset: effectiveTopInset
396
438
  },
397
439
  /* @__PURE__ */ React5.createElement(BottomSheetView, { style: styles2.content }, renderHeader ? renderHeader({ name: project?.name, description: project?.description }) : /* @__PURE__ */ React5.createElement(View4, { style: styles2.header }, /* @__PURE__ */ React5.createElement(View4, { style: { flex: 1, minWidth: 0 } }, project?.name && /* @__PURE__ */ React5.createElement(Text, { style: styles2.title, numberOfLines: 1 }, project.name), project?.description && /* @__PURE__ */ React5.createElement(Text, { style: styles2.subtitle, numberOfLines: 4 }, project.description)), /* @__PURE__ */ React5.createElement(Pressable, { style: styles2.iconBtn, onPress: toggleMinimize }, minimized ? /* @__PURE__ */ React5.createElement(ArrowUp, { size: 14, color: "rgba(255,255,255,0.6)" }) : /* @__PURE__ */ React5.createElement(ArrowDown, { size: 14, color: "rgba(255,255,255,0.6)" }))), children ?? /* @__PURE__ */ React5.createElement(DefaultLinesCarousel, null))
398
440
  );
@@ -476,8 +518,20 @@ import {
476
518
  } from "@gorhom/bottom-sheet";
477
519
  import Svg, { Circle } from "react-native-svg";
478
520
  import { ArrowDown as ArrowDown2, ArrowUp as ArrowUp2, Pause, Play, Plus } from "lucide-react-native";
479
- function LineDetailSheet({ snapPoints = ["35%", "75%"], renderHeader, children }) {
521
+ var renderBackdrop2 = (props) => /* @__PURE__ */ React6.createElement(
522
+ BottomSheetBackdrop2,
523
+ {
524
+ ...props,
525
+ appearsOnIndex: 1,
526
+ disappearsOnIndex: 0,
527
+ opacity: 0.4,
528
+ pressBehavior: "collapse"
529
+ }
530
+ );
531
+ function LineDetailSheet({ snapPoints = ["35%", "75%"], topInset, renderHeader, children }) {
480
532
  const { activeLine, isAddNoteOpen, controls } = useRetorBridge();
533
+ const { topInset: hudTopInset } = useHudContext();
534
+ const effectiveTopInset = topInset ?? hudTopInset;
481
535
  const sheetRef = useRef3(null);
482
536
  const snapPointsArr = useMemo4(() => snapPoints, [snapPoints]);
483
537
  const [minimized, setMinimized] = useState3(false);
@@ -518,18 +572,10 @@ function LineDetailSheet({ snapPoints = ["35%", "75%"], renderHeader, children }
518
572
  enableOverDrag: false,
519
573
  onChange: handleSheetChange,
520
574
  footerComponent: renderFooter,
521
- backdropComponent: (props) => /* @__PURE__ */ React6.createElement(
522
- BottomSheetBackdrop2,
523
- {
524
- ...props,
525
- appearsOnIndex: 1,
526
- disappearsOnIndex: 0,
527
- opacity: 0.4,
528
- pressBehavior: "collapse"
529
- }
530
- ),
575
+ backdropComponent: renderBackdrop2,
531
576
  handleIndicatorStyle: styles3.handle,
532
- backgroundComponent: BlurBackground
577
+ backgroundComponent: BlurBackground,
578
+ topInset: effectiveTopInset
533
579
  },
534
580
  activeLine && (children ?? /* @__PURE__ */ React6.createElement(DefaultLineTagList, { listHeader: header }))
535
581
  );
@@ -542,7 +588,8 @@ function DefaultHeader({
542
588
  return /* @__PURE__ */ React6.createElement(View5, { style: styles3.header }, /* @__PURE__ */ React6.createElement(View5, { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React6.createElement(Text2, { style: styles3.title, numberOfLines: 1 }, line.name), line.subtitle && /* @__PURE__ */ React6.createElement(Text2, { style: styles3.subtitle, numberOfLines: 1 }, line.subtitle), line.description && /* @__PURE__ */ React6.createElement(Text2, { style: styles3.description, numberOfLines: minimized ? 2 : 4 }, line.description)), /* @__PURE__ */ React6.createElement(View5, { style: styles3.headerActions }, /* @__PURE__ */ React6.createElement(AutoplayButton, null), /* @__PURE__ */ React6.createElement(Pressable2, { style: styles3.iconBtn, onPress: onToggleMinimize }, minimized ? /* @__PURE__ */ React6.createElement(ArrowUp2, { size: 14, color: "rgba(255,255,255,0.6)" }) : /* @__PURE__ */ React6.createElement(ArrowDown2, { size: 14, color: "rgba(255,255,255,0.6)" }))));
543
589
  }
544
590
  function AutoplayButton() {
545
- const { isPlaying, progress, controls } = useRetorBridge();
591
+ const { controls } = useRetorBridge();
592
+ const { isPlaying, progress } = useRetorProgress();
546
593
  const r = 12.5;
547
594
  const c = 2 * Math.PI * r;
548
595
  return /* @__PURE__ */ React6.createElement(Pressable2, { style: styles3.iconBtn, onPress: () => controls.toggleAutoplay() }, /* @__PURE__ */ React6.createElement(Svg, { width: 28, height: 28, style: StyleSheet5.absoluteFill }, /* @__PURE__ */ React6.createElement(
@@ -562,7 +609,8 @@ function AutoplayButton() {
562
609
  )), isPlaying ? /* @__PURE__ */ React6.createElement(Pause, { size: 11, color: "white", fill: "white" }) : /* @__PURE__ */ React6.createElement(Play, { size: 11, color: "white", fill: "white", style: { marginLeft: 1 } }));
563
610
  }
564
611
  function LineTagList({ children, listHeader }) {
565
- const { activeLine, closestTagId } = useRetorBridge();
612
+ const { activeLine } = useRetorBridge();
613
+ const { closestTagId } = useRetorProgress();
566
614
  const scrollRef = useRef3(null);
567
615
  const offsetsRef = useRef3(/* @__PURE__ */ new Map());
568
616
  const tags = useMemo4(
@@ -572,14 +620,16 @@ function LineTagList({ children, listHeader }) {
572
620
  useEffect3(() => {
573
621
  offsetsRef.current = /* @__PURE__ */ new Map();
574
622
  }, [activeLine?._id]);
623
+ const lastScrolledIdRef = useRef3(null);
575
624
  useEffect3(() => {
576
- if (!closestTagId) return;
625
+ if (!closestTagId || closestTagId === lastScrolledIdRef.current) return;
577
626
  const t = setTimeout(() => {
578
627
  const y = offsetsRef.current.get(closestTagId);
579
628
  if (y != null) {
580
- scrollRef.current?.scrollTo({ y, animated: true });
629
+ scrollRef.current?.scrollTo?.({ y, animated: true });
630
+ lastScrolledIdRef.current = closestTagId;
581
631
  }
582
- }, 60);
632
+ }, 300);
583
633
  return () => clearTimeout(t);
584
634
  }, [closestTagId]);
585
635
  if (!activeLine) return null;
@@ -693,13 +743,25 @@ import {
693
743
  BottomSheetView as BottomSheetView2
694
744
  } from "@gorhom/bottom-sheet";
695
745
  import { ArrowUp as ArrowUp3, X } from "lucide-react-native";
746
+ var renderBackdrop3 = (props) => /* @__PURE__ */ React7.createElement(
747
+ BottomSheetBackdrop3,
748
+ {
749
+ ...props,
750
+ appearsOnIndex: 0,
751
+ disappearsOnIndex: -1,
752
+ opacity: 0.6
753
+ }
754
+ );
696
755
  function AddNoteSheet({
697
756
  snapPoints = ["50%"],
757
+ topInset,
698
758
  maxLength = 280,
699
759
  placeholder = "Write a note...",
700
760
  renderForm
701
761
  }) {
702
762
  const { isAddNoteOpen, addNoteTagId, activeLine, closeAddNote, submitNote } = useRetorBridge();
763
+ const { topInset: hudTopInset } = useHudContext();
764
+ const effectiveTopInset = topInset ?? hudTopInset;
703
765
  const sheetRef = useRef4(null);
704
766
  const snapPointsArr = useMemo5(() => snapPoints, [snapPoints]);
705
767
  const [text, setText] = useState4("");
@@ -741,17 +803,10 @@ function AddNoteSheet({
741
803
  enablePanDownToClose: true,
742
804
  enableOverDrag: false,
743
805
  onDismiss: closeAddNote,
744
- backdropComponent: (props) => /* @__PURE__ */ React7.createElement(
745
- BottomSheetBackdrop3,
746
- {
747
- ...props,
748
- appearsOnIndex: 0,
749
- disappearsOnIndex: -1,
750
- opacity: 0.6
751
- }
752
- ),
806
+ backdropComponent: renderBackdrop3,
753
807
  handleIndicatorStyle: styles4.handle,
754
- backgroundComponent: BlurBackground
808
+ backgroundComponent: BlurBackground,
809
+ topInset: effectiveTopInset
755
810
  },
756
811
  /* @__PURE__ */ React7.createElement(BottomSheetView2, { style: styles4.content }, renderForm ? renderForm(formApi) : /* @__PURE__ */ React7.createElement(View6, { style: styles4.form }, /* @__PURE__ */ React7.createElement(View6, { style: styles4.headerRow }, /* @__PURE__ */ React7.createElement(View6, { style: { flex: 1 } }, activeLine && /* @__PURE__ */ React7.createElement(Text3, { style: styles4.title, numberOfLines: 1 }, activeLine.name), tag && /* @__PURE__ */ React7.createElement(Text3, { style: styles4.subtitle, numberOfLines: 1 }, tag.name)), /* @__PURE__ */ React7.createElement(Pressable3, { onPress: closeAddNote, style: styles4.closeBtn }, /* @__PURE__ */ React7.createElement(X, { size: 14, color: "rgba(255,255,255,0.6)" }))), /* @__PURE__ */ React7.createElement(
757
812
  BottomSheetTextInput,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retor/react-native",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "React Native SDK for embedding Retor 3D experiences",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",