@pictor-sdk/ui 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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -0
  3. package/dist/index.d.mts +833 -0
  4. package/dist/index.d.ts +833 -0
  5. package/dist/index.js +4620 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +4643 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +68 -0
  10. package/src/Editor.tsx +276 -0
  11. package/src/ToolPanelHost.tsx +98 -0
  12. package/src/components/Slider.tsx +172 -0
  13. package/src/components/StraightenRuler.tsx +57 -0
  14. package/src/components/ToolRail.tsx +58 -0
  15. package/src/components/TopBar.tsx +53 -0
  16. package/src/composer/TextComposer.tsx +540 -0
  17. package/src/composer/keyboardLayout.ts +130 -0
  18. package/src/composer/useTextSession.ts +230 -0
  19. package/src/gestures/CropGestureSurface.tsx +270 -0
  20. package/src/gestures/CropGrid.tsx +24 -0
  21. package/src/gestures/DrawingSurface.tsx +77 -0
  22. package/src/gestures/EditorGestures.tsx +97 -0
  23. package/src/gestures/InteractionGuides.tsx +161 -0
  24. package/src/gestures/LayerGestureSurface.tsx +462 -0
  25. package/src/gestures/StoryMediaGestureSurface.tsx +517 -0
  26. package/src/gestures/TiltShiftGestureSurface.tsx +189 -0
  27. package/src/gestures/controller.ts +152 -0
  28. package/src/gestures/drawController.ts +104 -0
  29. package/src/gestures/interactionState.tsx +81 -0
  30. package/src/gestures/layerGestureWorklet.ts +198 -0
  31. package/src/gestures/mediaGestureWorklet.ts +159 -0
  32. package/src/gestures/routing.ts +42 -0
  33. package/src/gestures/settleCoordinator.ts +61 -0
  34. package/src/gestures/transformOwnership.ts +91 -0
  35. package/src/hostCallbacks.tsx +62 -0
  36. package/src/index.ts +72 -0
  37. package/src/panels/AddPhotoPanel.tsx +101 -0
  38. package/src/panels/AdjustPanel.tsx +158 -0
  39. package/src/panels/ColorPanel.tsx +193 -0
  40. package/src/panels/CropPanel.tsx +185 -0
  41. package/src/panels/DrawPanel.tsx +106 -0
  42. package/src/panels/FilterStrip.tsx +153 -0
  43. package/src/panels/LayersPanel.tsx +85 -0
  44. package/src/panels/LayoutPanel.tsx +55 -0
  45. package/src/panels/LuxPanel.tsx +122 -0
  46. package/src/panels/MediaPanel.tsx +73 -0
  47. package/src/panels/PerspectivePanel.tsx +157 -0
  48. package/src/panels/StickerPanel.tsx +78 -0
  49. package/src/panels/TextPanel.tsx +177 -0
  50. package/src/panels/TiltShiftPanel.tsx +146 -0
  51. package/src/panels/addPhotoController.ts +196 -0
  52. package/src/panels/cropMath.ts +37 -0
  53. package/src/panels/layoutMath.ts +58 -0
  54. package/src/panels/useTargetImageLayer.ts +41 -0
  55. package/src/theme.ts +61 -0
  56. package/src/uiState.tsx +57 -0
package/dist/index.js ADDED
@@ -0,0 +1,4620 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ AddPhotoPanel: () => AddPhotoPanel,
34
+ AdjustPanel: () => AdjustPanel,
35
+ CanvasStage: () => CanvasStage,
36
+ CropGestureSurface: () => CropGestureSurface,
37
+ CropGrid: () => CropGrid,
38
+ CropPanel: () => CropPanel,
39
+ DEFAULT_BRUSH: () => DEFAULT_BRUSH,
40
+ DrawPanel: () => DrawPanel,
41
+ DrawingSurface: () => DrawingSurface,
42
+ Editor: () => Editor,
43
+ EditorGestures: () => EditorGestures,
44
+ FilterStrip: () => FilterStrip,
45
+ HostCallbacksProvider: () => HostCallbacksProvider,
46
+ InteractionGuides: () => InteractionGuides,
47
+ InteractionProvider: () => InteractionProvider,
48
+ InvalidImageResultError: () => InvalidImageResultError,
49
+ LayerGestureSurface: () => LayerGestureSurface,
50
+ LayersPanel: () => LayersPanel,
51
+ LayoutPanel: () => LayoutPanel,
52
+ MediaPanel: () => MediaPanel,
53
+ POST_PROFILE: () => import_core34.POST_PROFILE,
54
+ STORY_PROFILE: () => import_core34.STORY_PROFILE,
55
+ STRAIGHTEN_LIMIT: () => STRAIGHTEN_LIMIT,
56
+ Slider: () => Slider,
57
+ StickerPanel: () => StickerPanel,
58
+ StoryMediaGestureSurface: () => StoryMediaGestureSurface,
59
+ StraightenRuler: () => StraightenRuler,
60
+ TextComposer: () => TextComposer,
61
+ TextComposerHost: () => TextComposerHost,
62
+ TextPanel: () => TextPanel,
63
+ ThemeContext: () => ThemeContext,
64
+ ToolPanelHost: () => ToolPanelHost,
65
+ ToolRail: () => ToolRail,
66
+ TopBar: () => TopBar,
67
+ UiStateProvider: () => UiStateProvider,
68
+ canvasSizeForAspect: () => canvasSizeForAspect,
69
+ cellToTransformPatch: () => cellToTransformPatch,
70
+ centeredCropForAspect: () => centeredCropForAspect,
71
+ createAddPhotoController: () => createAddPhotoController,
72
+ createDrawController: () => createDrawController,
73
+ createGestureController: () => createGestureController,
74
+ createUiStateStore: () => createUiStateStore,
75
+ defaultTheme: () => defaultTheme,
76
+ layoutCells: () => layoutCells,
77
+ mergeSlots: () => mergeSlots,
78
+ mergeTheme: () => mergeTheme,
79
+ radiansToStraighten: () => radiansToStraighten,
80
+ resolveGestureSurface: () => resolveGestureSurface,
81
+ resolvePanel: () => resolvePanel,
82
+ straightenToRadians: () => straightenToRadians,
83
+ useBaseImageLayer: () => useBaseImageLayer,
84
+ useBaseMediaLayer: () => useBaseMediaLayer,
85
+ useHostCallbacks: () => useHostCallbacks,
86
+ useInteractionState: () => useInteractionState,
87
+ useTargetImageLayer: () => useTargetImageLayer,
88
+ useTextSession: () => useTextSession,
89
+ useTheme: () => useTheme,
90
+ useUiState: () => useUiState
91
+ });
92
+ module.exports = __toCommonJS(index_exports);
93
+
94
+ // src/Editor.tsx
95
+ var import_core30 = require("@pictor-sdk/core");
96
+ var import_skia10 = require("@pictor-sdk/skia");
97
+ var import_react22 = __toESM(require("react"));
98
+ var import_react_native27 = require("react-native");
99
+ var import_react_native_gesture_handler8 = require("react-native-gesture-handler");
100
+
101
+ // src/hostCallbacks.tsx
102
+ var import_react = require("react");
103
+ var import_jsx_runtime = require("react/jsx-runtime");
104
+ var Context = (0, import_react.createContext)(null);
105
+ function HostCallbacksProvider({
106
+ value,
107
+ children
108
+ }) {
109
+ const ref = (0, import_react.useRef)(value);
110
+ ref.current = value;
111
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Context.Provider, { value: ref, children });
112
+ }
113
+ function useHostCallbacks() {
114
+ const fallback = (0, import_react.useRef)({});
115
+ return (0, import_react.useContext)(Context) ?? fallback;
116
+ }
117
+
118
+ // src/gestures/DrawingSurface.tsx
119
+ var import_core2 = require("@pictor-sdk/core");
120
+ var import_skia = require("@pictor-sdk/skia");
121
+ var import_react3 = require("react");
122
+ var import_react_native = require("react-native");
123
+ var import_react_native_gesture_handler = require("react-native-gesture-handler");
124
+ var import_react_native_reanimated = __toESM(require("react-native-reanimated"));
125
+ var import_react_native_worklets = require("react-native-worklets");
126
+
127
+ // src/uiState.tsx
128
+ var import_react2 = require("react");
129
+ var import_jsx_runtime2 = require("react/jsx-runtime");
130
+ function createUiStateStore() {
131
+ const values = /* @__PURE__ */ new Map();
132
+ const listeners = /* @__PURE__ */ new Set();
133
+ return {
134
+ get: (key, fallback) => values.has(key) ? values.get(key) : fallback,
135
+ set(key, value) {
136
+ values.set(key, value);
137
+ for (const l of [...listeners]) l();
138
+ },
139
+ subscribe(listener) {
140
+ listeners.add(listener);
141
+ return () => listeners.delete(listener);
142
+ }
143
+ };
144
+ }
145
+ var UiStateContext = (0, import_react2.createContext)(null);
146
+ function UiStateProvider({ children }) {
147
+ const ref = (0, import_react2.useRef)(null);
148
+ if (!ref.current) ref.current = createUiStateStore();
149
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(UiStateContext.Provider, { value: ref.current, children });
150
+ }
151
+ function useUiState(key, fallback) {
152
+ const store = (0, import_react2.useContext)(UiStateContext);
153
+ if (!store) throw new Error("useUiState must be used inside <Editor> (UiStateProvider)");
154
+ const value = (0, import_react2.useSyncExternalStore)(
155
+ store.subscribe,
156
+ () => store.get(key, fallback),
157
+ () => store.get(key, fallback)
158
+ );
159
+ const set = (0, import_react2.useCallback)((v) => store.set(key, v), [store, key]);
160
+ return [value, set];
161
+ }
162
+
163
+ // src/gestures/drawController.ts
164
+ var import_core = require("@pictor-sdk/core");
165
+ function createDrawController(store, getMetrics, getBrush) {
166
+ let drawing = false;
167
+ let layerId = null;
168
+ let points = [];
169
+ let baseStrokes = [];
170
+ function toDocPoint(xDp, yDp) {
171
+ const metrics = getMetrics();
172
+ if (!metrics || metrics.width === 0 || metrics.height === 0) return null;
173
+ return { x: xDp / metrics.width, y: yDp / metrics.height };
174
+ }
175
+ function ensureDrawLayer() {
176
+ const doc = store.getState().document;
177
+ for (let i = doc.layers.length - 1; i >= 0; i--) {
178
+ const layer = doc.layers[i];
179
+ if (layer.type === "draw" && !layer.locked && !layer.hidden) return layer.id;
180
+ }
181
+ const id = (0, import_core.createId)("draw");
182
+ store.dispatch({
183
+ type: "layer/add",
184
+ layer: {
185
+ id,
186
+ type: "draw",
187
+ transform: { x: 0.5, y: 0.5, scale: 1, rotation: 0 },
188
+ opacity: 1,
189
+ blendMode: "normal",
190
+ data: { strokes: [] }
191
+ }
192
+ });
193
+ return id;
194
+ }
195
+ function currentStroke() {
196
+ const brush = getBrush();
197
+ return { points, color: brush.color, width: brush.width, brush: brush.brush };
198
+ }
199
+ function patch() {
200
+ if (!layerId) return;
201
+ store.dispatch({
202
+ type: "layer/patchData",
203
+ id: layerId,
204
+ patch: { strokes: [...baseStrokes, currentStroke()] }
205
+ });
206
+ }
207
+ return {
208
+ begin(xDp, yDp) {
209
+ const point = toDocPoint(xDp, yDp);
210
+ if (!point || drawing) return;
211
+ drawing = true;
212
+ store.dispatch({ type: "history/beginBatch", label: "draw" });
213
+ layerId = ensureDrawLayer();
214
+ const layer = store.getState().document.layers.find((l) => l.id === layerId);
215
+ baseStrokes = layer?.data?.strokes ?? [];
216
+ points = [point];
217
+ patch();
218
+ },
219
+ move(xDp, yDp) {
220
+ if (!drawing) return;
221
+ const point = toDocPoint(xDp, yDp);
222
+ if (!point) return;
223
+ const last = points[points.length - 1];
224
+ if (last && Math.hypot(point.x - last.x, point.y - last.y) < 3e-3) return;
225
+ points = [...points, point];
226
+ patch();
227
+ },
228
+ end() {
229
+ if (!drawing) return;
230
+ drawing = false;
231
+ store.dispatch({ type: "history/endBatch" });
232
+ layerId = null;
233
+ points = [];
234
+ baseStrokes = [];
235
+ },
236
+ isDrawing: () => drawing
237
+ };
238
+ }
239
+
240
+ // src/gestures/DrawingSurface.tsx
241
+ var import_jsx_runtime3 = require("react/jsx-runtime");
242
+ var DEFAULT_BRUSH = { brush: "pen", color: "#ffffff", width: 0.012 };
243
+ function DrawingSurface() {
244
+ const store = (0, import_core2.useEditorStore)();
245
+ const { document } = (0, import_core2.useEditor)();
246
+ const renderer = (0, import_skia.useRenderer)();
247
+ const metrics = (0, import_skia.useCanvasMetrics)();
248
+ const metricsRef = (0, import_react3.useRef)(metrics);
249
+ metricsRef.current = metrics;
250
+ const [brush, setBrush] = useUiState("draw.brush", DEFAULT_BRUSH);
251
+ const [eyedropper, setEyedropper] = useUiState("draw.eyedropper", false);
252
+ const brushRef = (0, import_react3.useRef)(brush);
253
+ brushRef.current = brush;
254
+ const stateRef = (0, import_react3.useRef)({ eyedropper, document, setBrush, setEyedropper });
255
+ stateRef.current = { eyedropper, document, setBrush, setEyedropper };
256
+ const controller = (0, import_react3.useMemo)(
257
+ () => createDrawController(store, () => metricsRef.current, () => brushRef.current),
258
+ [store]
259
+ );
260
+ const gesture = (0, import_react3.useMemo)(() => {
261
+ const sample = async (x, y) => {
262
+ const m = metricsRef.current;
263
+ const s = stateRef.current;
264
+ if (!m || !s.eyedropper) return;
265
+ const color = await (0, import_skia.samplePixelColor)(renderer, s.document, {
266
+ x: x / m.width,
267
+ y: y / m.height
268
+ });
269
+ if (color) s.setBrush({ ...brushRef.current, color });
270
+ s.setEyedropper(false);
271
+ };
272
+ const beginOrSample = (x, y) => {
273
+ if (stateRef.current.eyedropper) {
274
+ void sample(x, y);
275
+ } else {
276
+ controller.begin(x, y);
277
+ }
278
+ };
279
+ return import_react_native_gesture_handler.Gesture.Pan().minDistance(0).onStart((e) => {
280
+ "worklet";
281
+ (0, import_react_native_worklets.runOnJS)(beginOrSample)(e.x, e.y);
282
+ }).onUpdate((e) => {
283
+ "worklet";
284
+ (0, import_react_native_worklets.runOnJS)(controller.move)(e.x, e.y);
285
+ }).onFinalize(() => {
286
+ "worklet";
287
+ (0, import_react_native_worklets.runOnJS)(controller.end)();
288
+ });
289
+ }, [controller, renderer]);
290
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_gesture_handler.GestureDetector, { gesture, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_reanimated.default.View, { style: import_react_native.StyleSheet.absoluteFill }) });
291
+ }
292
+
293
+ // src/gestures/CropGestureSurface.tsx
294
+ var import_core5 = require("@pictor-sdk/core");
295
+ var import_skia2 = require("@pictor-sdk/skia");
296
+ var import_react6 = require("react");
297
+ var import_react_native2 = require("react-native");
298
+ var import_react_native_gesture_handler2 = require("react-native-gesture-handler");
299
+ var import_react_native_reanimated2 = __toESM(require("react-native-reanimated"));
300
+ var import_react_native_worklets2 = require("react-native-worklets");
301
+
302
+ // src/panels/useTargetImageLayer.ts
303
+ var import_core3 = require("@pictor-sdk/core");
304
+ var import_react4 = require("react");
305
+ function useTargetImageLayer() {
306
+ const { document, selection } = (0, import_core3.useEditor)();
307
+ const pinned = (0, import_react4.useRef)(null);
308
+ if (pinned.current === null) {
309
+ pinned.current = (0, import_core3.resolveTargetImageLayer)(document, selection)?.id ?? null;
310
+ }
311
+ return (0, import_core3.pinnedTargetImageLayer)(document, pinned.current) ?? null;
312
+ }
313
+
314
+ // src/gestures/CropGestureSurface.tsx
315
+ var import_core6 = require("@pictor-sdk/core");
316
+
317
+ // src/gestures/interactionState.tsx
318
+ var import_core4 = require("@pictor-sdk/core");
319
+ var import_react5 = require("react");
320
+ var import_jsx_runtime4 = require("react/jsx-runtime");
321
+ var Context2 = (0, import_react5.createContext)(null);
322
+ function InteractionProvider({ children }) {
323
+ const [preview, setPreviewValue] = (0, import_react5.useState)(null);
324
+ const [cropActive, setCropActive] = (0, import_react5.useState)(false);
325
+ const [layerDragActive, setLayerDragActive] = (0, import_react5.useState)(false);
326
+ const [overDeleteTarget, setOverDeleteTarget] = (0, import_react5.useState)(false);
327
+ const [snap, setSnapValue] = (0, import_react5.useState)(import_core4.NO_SNAP);
328
+ const setPreview = (0, import_react5.useCallback)((value2) => setPreviewValue(value2), []);
329
+ const setSnap = (0, import_react5.useCallback)((value2) => {
330
+ setSnapValue(
331
+ (current) => current.centerX === value2.centerX && current.centerY === value2.centerY && current.rotation === value2.rotation ? current : value2
332
+ );
333
+ }, []);
334
+ const value = (0, import_react5.useMemo)(
335
+ () => ({
336
+ previews: preview ? [preview] : [],
337
+ setPreview,
338
+ cropActive,
339
+ setCropActive,
340
+ layerDragActive,
341
+ setLayerDragActive,
342
+ overDeleteTarget,
343
+ setOverDeleteTarget,
344
+ snap,
345
+ setSnap
346
+ }),
347
+ [preview, setPreview, cropActive, layerDragActive, overDeleteTarget, snap, setSnap]
348
+ );
349
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Context2.Provider, { value, children });
350
+ }
351
+ function useInteractionState() {
352
+ const value = (0, import_react5.useContext)(Context2);
353
+ if (!value) throw new Error("interaction components must be used inside <Editor>");
354
+ return value;
355
+ }
356
+
357
+ // src/gestures/CropGestureSurface.tsx
358
+ var import_jsx_runtime5 = require("react/jsx-runtime");
359
+ function CropGestureSurface({ fit, maxScale, rotationEnabled, onInteractionChange }) {
360
+ const { document, dispatch, profile } = (0, import_core6.useEditor)();
361
+ const layer = useTargetImageLayer();
362
+ const metrics = (0, import_skia2.useCanvasMetrics)();
363
+ const interaction = useInteractionState();
364
+ const config = profile.interaction?.crop;
365
+ const source = layer?.data?.intrinsic;
366
+ const resolvedFit = fit ?? config?.fit ?? "contain";
367
+ const resolvedMax = maxScale ?? config?.maxScale ?? 8;
368
+ const resolvedRotation = rotationEnabled ?? config?.rotationEnabled ?? false;
369
+ const liveX = (0, import_react_native_reanimated2.useSharedValue)(layer?.transform.x ?? 0.5);
370
+ const liveY = (0, import_react_native_reanimated2.useSharedValue)(layer?.transform.y ?? 0.5);
371
+ const liveScale = (0, import_react_native_reanimated2.useSharedValue)(layer?.transform.scale ?? 1);
372
+ const baseX = (0, import_react_native_reanimated2.useSharedValue)(layer?.transform.x ?? 0.5);
373
+ const baseY = (0, import_react_native_reanimated2.useSharedValue)(layer?.transform.y ?? 0.5);
374
+ const baseScale = (0, import_react_native_reanimated2.useSharedValue)(layer?.transform.scale ?? 1);
375
+ const liveRotation = (0, import_react_native_reanimated2.useSharedValue)(layer?.transform.rotation ?? 0);
376
+ const baseRotation = (0, import_react_native_reanimated2.useSharedValue)(layer?.transform.rotation ?? 0);
377
+ const panX = (0, import_react_native_reanimated2.useSharedValue)(0);
378
+ const panY = (0, import_react_native_reanimated2.useSharedValue)(0);
379
+ const pinchScale = (0, import_react_native_reanimated2.useSharedValue)(1);
380
+ const rotateDelta = (0, import_react_native_reanimated2.useSharedValue)(0);
381
+ const focalX = (0, import_react_native_reanimated2.useSharedValue)(0.5);
382
+ const focalY = (0, import_react_native_reanimated2.useSharedValue)(0.5);
383
+ const activeRecognizers = (0, import_react_native_reanimated2.useSharedValue)(0);
384
+ const commitTimer = (0, import_react6.useRef)(null);
385
+ const minScale = source ? (0, import_core5.cropFitScale)(source, document.size, resolvedFit, layer?.transform.rotation ?? 0) : 1;
386
+ (0, import_react6.useEffect)(() => {
387
+ if (!layer) return;
388
+ liveX.value = layer.transform.x;
389
+ liveY.value = layer.transform.y;
390
+ liveScale.value = layer.transform.scale;
391
+ liveRotation.value = layer.transform.rotation;
392
+ }, [layer?.transform.x, layer?.transform.y, layer?.transform.scale, layer?.transform.rotation]);
393
+ const previewTransform = (0, import_react_native_reanimated2.useDerivedValue)(() => {
394
+ const ratio = liveScale.value / Math.max(baseScale.value, 1e-6);
395
+ return [
396
+ { translateX: liveX.value * document.size.width },
397
+ { translateY: liveY.value * document.size.height },
398
+ { rotate: liveRotation.value - baseRotation.value },
399
+ { scale: ratio },
400
+ { translateX: -baseX.value * document.size.width },
401
+ { translateY: -baseY.value * document.size.height }
402
+ ];
403
+ }, [document.size.width, document.size.height]);
404
+ (0, import_react6.useEffect)(() => {
405
+ if (!layer) return;
406
+ interaction.setPreview({ layerId: layer.id, transform: previewTransform });
407
+ return () => interaction.setPreview(null);
408
+ }, [layer?.id, previewTransform, interaction.setPreview]);
409
+ const setActive = (active) => {
410
+ interaction.setCropActive(active);
411
+ onInteractionChange?.(active);
412
+ };
413
+ const commit = (candidate) => {
414
+ if (!layer || !source) return;
415
+ if (commitTimer.current) clearTimeout(commitTimer.current);
416
+ const settledMinScale = (0, import_core5.cropFitScale)(source, document.size, resolvedFit, candidate.rotation);
417
+ const bounds = (0, import_core5.cropPanBounds)(source, document.size, candidate.scale, settledMinScale, resolvedMax, candidate.rotation);
418
+ const settled = (0, import_core5.clampCropTransform)(candidate, bounds);
419
+ liveX.value = (0, import_react_native_reanimated2.withTiming)(settled.x, { duration: 160 });
420
+ liveY.value = (0, import_react_native_reanimated2.withTiming)(settled.y, { duration: 160 });
421
+ liveScale.value = (0, import_react_native_reanimated2.withTiming)(settled.scale, { duration: 160 });
422
+ liveRotation.value = (0, import_react_native_reanimated2.withTiming)(settled.rotation, { duration: 160 });
423
+ commitTimer.current = setTimeout(() => {
424
+ dispatch({
425
+ type: "layer/transform",
426
+ id: layer.id,
427
+ patch: {
428
+ x: settled.x,
429
+ y: settled.y,
430
+ scale: settled.scale,
431
+ ...resolvedRotation ? { rotation: settled.rotation } : {}
432
+ }
433
+ });
434
+ baseX.value = settled.x;
435
+ baseY.value = settled.y;
436
+ baseScale.value = settled.scale;
437
+ baseRotation.value = settled.rotation;
438
+ liveX.value = settled.x;
439
+ liveY.value = settled.y;
440
+ liveScale.value = settled.scale;
441
+ liveRotation.value = settled.rotation;
442
+ setActive(false);
443
+ }, 165);
444
+ };
445
+ const toggleZoom = (xDp, yDp) => {
446
+ if (!layer || !source || !metrics) return;
447
+ baseX.value = layer.transform.x;
448
+ baseY.value = layer.transform.y;
449
+ baseScale.value = layer.transform.scale;
450
+ baseRotation.value = layer.transform.rotation;
451
+ liveX.value = layer.transform.x;
452
+ liveY.value = layer.transform.y;
453
+ liveScale.value = layer.transform.scale;
454
+ liveRotation.value = layer.transform.rotation;
455
+ setActive(true);
456
+ const atMinimum = Math.abs(layer.transform.scale - minScale) < 0.01;
457
+ const targetScale = atMinimum ? Math.min(resolvedMax, minScale * (config?.doubleTapScale ?? 2)) : minScale;
458
+ commit((0, import_core5.focalZoomTransform)(layer.transform, targetScale, {
459
+ x: xDp / metrics.width,
460
+ y: yDp / metrics.height
461
+ }));
462
+ };
463
+ const gesture = (0, import_react6.useMemo)(() => {
464
+ if (!metrics || !source || !layer) return import_react_native_gesture_handler2.Gesture.Tap();
465
+ const begin = () => {
466
+ "worklet";
467
+ if (activeRecognizers.value === 0) {
468
+ baseX.value = liveX.value;
469
+ baseY.value = liveY.value;
470
+ baseScale.value = liveScale.value;
471
+ baseRotation.value = liveRotation.value;
472
+ panX.value = 0;
473
+ panY.value = 0;
474
+ pinchScale.value = 1;
475
+ rotateDelta.value = 0;
476
+ (0, import_react_native_worklets2.runOnJS)(setActive)(true);
477
+ }
478
+ activeRecognizers.value += 1;
479
+ };
480
+ const updateLive = () => {
481
+ "worklet";
482
+ const rotation = baseRotation.value + rotateDelta.value;
483
+ const naturalWidth = document.size.width;
484
+ const naturalHeight = document.size.width * (source.height / source.width);
485
+ const cos = Math.abs(Math.cos(rotation));
486
+ const sin = Math.abs(Math.sin(rotation));
487
+ const rotatedWidth = naturalWidth * cos + naturalHeight * sin;
488
+ const rotatedHeight = naturalWidth * sin + naturalHeight * cos;
489
+ const widthScale = document.size.width / rotatedWidth;
490
+ const heightScale = document.size.height / rotatedHeight;
491
+ const dynamicMin = resolvedFit === "contain" ? Math.min(widthScale, heightScale) : Math.max(widthScale, heightScale);
492
+ const rawScale = baseScale.value * pinchScale.value;
493
+ const resistance = config?.resistance ?? 0.35;
494
+ const scale = rawScale < dynamicMin ? dynamicMin - (dynamicMin - rawScale) * resistance : rawScale > resolvedMax ? resolvedMax + (rawScale - resolvedMax) * resistance : rawScale;
495
+ const ratio = scale / Math.max(baseScale.value, 1e-6);
496
+ const zoomX = focalX.value - (focalX.value - baseX.value) * ratio;
497
+ const zoomY = focalY.value - (focalY.value - baseY.value) * ratio;
498
+ liveRotation.value = rotation;
499
+ const dx = Math.max(0, (rotatedWidth * scale - document.size.width) / (2 * document.size.width));
500
+ const dy = Math.max(0, (rotatedHeight * scale - document.size.height) / (2 * document.size.height));
501
+ liveScale.value = scale;
502
+ const rawX = zoomX + panX.value;
503
+ const rawY = zoomY + panY.value;
504
+ const minX = 0.5 - dx;
505
+ const maxX = 0.5 + dx;
506
+ const minY = 0.5 - dy;
507
+ const maxY = 0.5 + dy;
508
+ liveX.value = rawX < minX ? minX - (minX - rawX) * resistance : rawX > maxX ? maxX + (rawX - maxX) * resistance : rawX;
509
+ liveY.value = rawY < minY ? minY - (minY - rawY) * resistance : rawY > maxY ? maxY + (rawY - maxY) * resistance : rawY;
510
+ };
511
+ const finish = () => {
512
+ "worklet";
513
+ activeRecognizers.value = Math.max(0, activeRecognizers.value - 1);
514
+ if (activeRecognizers.value === 0) {
515
+ (0, import_react_native_worklets2.runOnJS)(commit)({
516
+ x: liveX.value,
517
+ y: liveY.value,
518
+ scale: liveScale.value,
519
+ rotation: liveRotation.value,
520
+ ...layer.transform.anchor ? { anchor: layer.transform.anchor } : {},
521
+ ...layer.transform.flipX !== void 0 ? { flipX: layer.transform.flipX } : {},
522
+ ...layer.transform.flipY !== void 0 ? { flipY: layer.transform.flipY } : {}
523
+ });
524
+ }
525
+ };
526
+ const pan = import_react_native_gesture_handler2.Gesture.Pan().minPointers(1).onStart(begin).onUpdate((event) => {
527
+ "worklet";
528
+ panX.value = event.translationX / metrics.width;
529
+ panY.value = event.translationY / metrics.height;
530
+ updateLive();
531
+ }).onFinalize(finish);
532
+ const pinch = import_react_native_gesture_handler2.Gesture.Pinch().onStart(begin).onUpdate((event) => {
533
+ "worklet";
534
+ pinchScale.value = event.scale;
535
+ focalX.value = event.focalX / metrics.width;
536
+ focalY.value = event.focalY / metrics.height;
537
+ updateLive();
538
+ }).onFinalize(finish);
539
+ const rotate = import_react_native_gesture_handler2.Gesture.Rotation().enabled(resolvedRotation).onStart(begin).onUpdate((event) => {
540
+ "worklet";
541
+ rotateDelta.value = event.rotation;
542
+ updateLive();
543
+ }).onFinalize(finish);
544
+ const doubleTap = import_react_native_gesture_handler2.Gesture.Tap().numberOfTaps(2).onEnd((event) => {
545
+ "worklet";
546
+ (0, import_react_native_worklets2.runOnJS)(toggleZoom)(event.x, event.y);
547
+ });
548
+ return import_react_native_gesture_handler2.Gesture.Exclusive(doubleTap, import_react_native_gesture_handler2.Gesture.Simultaneous(pan, pinch, rotate));
549
+ }, [metrics, source, layer, resolvedFit, resolvedMax, resolvedRotation, minScale, document.size.width, document.size.height, config?.resistance, config?.doubleTapScale]);
550
+ if (!layer) return null;
551
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native_gesture_handler2.GestureDetector, { gesture, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native_reanimated2.default.View, { style: import_react_native2.StyleSheet.absoluteFill }) });
552
+ }
553
+
554
+ // src/gestures/TiltShiftGestureSurface.tsx
555
+ var import_core7 = require("@pictor-sdk/core");
556
+ var import_skia3 = require("@pictor-sdk/skia");
557
+ var import_react7 = require("react");
558
+ var import_react_native3 = require("react-native");
559
+ var import_react_native_gesture_handler3 = require("react-native-gesture-handler");
560
+ var import_react_native_reanimated3 = __toESM(require("react-native-reanimated"));
561
+ var import_react_native_worklets3 = require("react-native-worklets");
562
+ var import_jsx_runtime6 = require("react/jsx-runtime");
563
+ function TiltShiftGestureSurface() {
564
+ const { dispatch } = (0, import_core7.useEditor)();
565
+ const layer = useTargetImageLayer();
566
+ const metrics = (0, import_skia3.useCanvasMetrics)();
567
+ const tilt = layer?.data?.tiltShift;
568
+ const active = (0, import_core7.needsTiltShift)(tilt);
569
+ const origin = (0, import_react7.useRef)(null);
570
+ const live = (0, import_react7.useRef)(null);
571
+ live.current = active ? tilt : null;
572
+ const width = metrics?.width ?? 0;
573
+ const height = metrics?.height ?? 0;
574
+ const oX = (0, import_react_native_reanimated3.useSharedValue)(0);
575
+ const oY = (0, import_react_native_reanimated3.useSharedValue)(0);
576
+ const oR = (0, import_react_native_reanimated3.useSharedValue)(0);
577
+ const oRot = (0, import_react_native_reanimated3.useSharedValue)(0);
578
+ const oLinear = (0, import_react_native_reanimated3.useSharedValue)(0);
579
+ const visible = (0, import_react_native_reanimated3.useSharedValue)(0);
580
+ const dx = (0, import_react_native_reanimated3.useSharedValue)(0);
581
+ const dy = (0, import_react_native_reanimated3.useSharedValue)(0);
582
+ const pinch = (0, import_react_native_reanimated3.useSharedValue)(1);
583
+ const twist = (0, import_react_native_reanimated3.useSharedValue)(0);
584
+ const activeCount = (0, import_react_native_reanimated3.useSharedValue)(0);
585
+ const capture = (0, import_react7.useMemo)(
586
+ () => () => {
587
+ const from = live.current;
588
+ if (!from || origin.current) return;
589
+ origin.current = from;
590
+ oX.value = from.x * width;
591
+ oY.value = from.y * height;
592
+ oR.value = from.radius * width;
593
+ oRot.value = from.rotation;
594
+ oLinear.value = from.mode === "linear" ? 1 : 0;
595
+ },
596
+ [width, height, oX, oY, oR, oRot, oLinear]
597
+ );
598
+ const commit = (0, import_react7.useMemo)(
599
+ () => (tx, ty, scale, rotation) => {
600
+ const from = origin.current;
601
+ origin.current = null;
602
+ if (!from || !layer || width <= 0 || height <= 0) return;
603
+ let next = (0, import_core7.moveTiltCenter)(from, tx / width, ty / height);
604
+ next = (0, import_core7.scaleTiltRadius)({ ...next, radius: from.radius, feather: from.feather }, scale);
605
+ next = (0, import_core7.rotateTiltBand)({ ...next, rotation: from.rotation }, rotation);
606
+ dispatch({
607
+ type: "layer/patchData",
608
+ id: layer.id,
609
+ patch: (0, import_core7.tiltShiftPatch)(next)
610
+ });
611
+ },
612
+ [dispatch, layer, width, height]
613
+ );
614
+ const gesture = (0, import_react7.useMemo)(() => {
615
+ const begin = () => {
616
+ "worklet";
617
+ activeCount.value += 1;
618
+ visible.value = 1;
619
+ (0, import_react_native_worklets3.runOnJS)(capture)();
620
+ };
621
+ const end = () => {
622
+ "worklet";
623
+ activeCount.value = Math.max(0, activeCount.value - 1);
624
+ if (activeCount.value > 0) return;
625
+ visible.value = 0;
626
+ (0, import_react_native_worklets3.runOnJS)(commit)(dx.value, dy.value, pinch.value, twist.value);
627
+ dx.value = 0;
628
+ dy.value = 0;
629
+ pinch.value = 1;
630
+ twist.value = 0;
631
+ };
632
+ const pan = import_react_native_gesture_handler3.Gesture.Pan().enabled(active).onStart(begin).onUpdate((e) => {
633
+ "worklet";
634
+ dx.value = e.translationX;
635
+ dy.value = e.translationY;
636
+ }).onFinalize(end);
637
+ const pinchGesture = import_react_native_gesture_handler3.Gesture.Pinch().enabled(active).onStart(begin).onUpdate((e) => {
638
+ "worklet";
639
+ pinch.value = e.scale;
640
+ }).onFinalize(end);
641
+ const rotation = import_react_native_gesture_handler3.Gesture.Rotation().enabled(active).onStart(begin).onUpdate((e) => {
642
+ "worklet";
643
+ twist.value = e.rotation;
644
+ }).onFinalize(end);
645
+ return import_react_native_gesture_handler3.Gesture.Simultaneous(pan, pinchGesture, rotation);
646
+ }, [active, capture, commit, activeCount, visible, dx, dy, pinch, twist]);
647
+ const ringStyle = (0, import_react_native_reanimated3.useAnimatedStyle)(() => {
648
+ const r = Math.max(1, oR.value * pinch.value);
649
+ const cx = oX.value + dx.value;
650
+ const cy = oY.value + dy.value;
651
+ const band = oLinear.value > 0.5;
652
+ return {
653
+ opacity: visible.value,
654
+ left: band ? -width : cx - r,
655
+ top: cy - r,
656
+ width: band ? width * 3 : r * 2,
657
+ height: r * 2,
658
+ borderRadius: band ? 0 : r,
659
+ transform: [{ rotate: `${oRot.value + twist.value}rad` }]
660
+ };
661
+ });
662
+ if (!active) return null;
663
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_gesture_handler3.GestureDetector, { gesture, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_reanimated3.default.View, { style: import_react_native3.StyleSheet.absoluteFill, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_reanimated3.default.View, { pointerEvents: "none", style: [styles.ring, ringStyle] }) }) });
664
+ }
665
+ var styles = import_react_native3.StyleSheet.create({
666
+ ring: {
667
+ position: "absolute",
668
+ borderWidth: 1.5,
669
+ borderColor: "rgba(255,255,255,0.9)"
670
+ }
671
+ });
672
+
673
+ // src/gestures/CropGrid.tsx
674
+ var import_react8 = __toESM(require("react"));
675
+ var import_react_native4 = require("react-native");
676
+ var import_jsx_runtime7 = require("react/jsx-runtime");
677
+ function CropGrid({ always = false }) {
678
+ const { cropActive } = useInteractionState();
679
+ if (!always && !cropActive) return null;
680
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native4.View, { pointerEvents: "none", style: import_react_native4.StyleSheet.absoluteFill, children: [1 / 3, 2 / 3].map((fraction) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react8.default.Fragment, { children: [
681
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native4.View, { style: [styles2.vertical, { left: `${fraction * 100}%` }] }),
682
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native4.View, { style: [styles2.horizontal, { top: `${fraction * 100}%` }] })
683
+ ] }, fraction)) });
684
+ }
685
+ var styles2 = import_react_native4.StyleSheet.create({
686
+ vertical: { position: "absolute", top: 0, bottom: 0, width: 1, backgroundColor: "#ffffff88" },
687
+ horizontal: { position: "absolute", left: 0, right: 0, height: 1, backgroundColor: "#ffffff88" }
688
+ });
689
+
690
+ // src/gestures/LayerGestureSurface.tsx
691
+ var import_core8 = require("@pictor-sdk/core");
692
+ var import_skia4 = require("@pictor-sdk/skia");
693
+ var import_react9 = require("react");
694
+ var import_react_native5 = require("react-native");
695
+ var import_react_native_gesture_handler4 = require("react-native-gesture-handler");
696
+ var import_react_native_reanimated4 = __toESM(require("react-native-reanimated"));
697
+ var import_react_native_worklets4 = require("react-native-worklets");
698
+
699
+ // src/gestures/layerGestureWorklet.ts
700
+ function composeLayerGestureUI(base, input, aspect, config, previous, settle) {
701
+ "worklet";
702
+ const requested = base.scale * input.pinch;
703
+ let scale;
704
+ if (settle) {
705
+ scale = Math.min(config.maxScale, Math.max(config.minScale, requested));
706
+ } else if (requested < config.minScale) {
707
+ scale = config.minScale - (config.minScale - requested) * config.scaleResistance;
708
+ } else if (requested > config.maxScale) {
709
+ scale = config.maxScale + (requested - config.maxScale) * config.scaleResistance;
710
+ } else {
711
+ scale = requested;
712
+ }
713
+ const ratio = scale / Math.max(base.scale, 1e-9);
714
+ const rotationDelta = config.rotationEnabled ? input.rotate : 0;
715
+ const dx = base.x - input.focalX;
716
+ const dy = (base.y - input.focalY) * aspect;
717
+ const cos = Math.cos(rotationDelta);
718
+ const sin = Math.sin(rotationDelta);
719
+ const rx = (dx * cos - dy * sin) * ratio;
720
+ const ry = (dx * sin + dy * cos) * ratio;
721
+ let x = input.focalX + rx + input.panX;
722
+ let y = input.focalY + ry / aspect + input.panY;
723
+ let rotation = base.rotation + rotationDelta;
724
+ let snapCenterX = false;
725
+ let snapCenterY = false;
726
+ let snapRotation2 = false;
727
+ if (config.centerSnap !== false) {
728
+ const engage = config.centerSnap;
729
+ const release = Math.max(engage, config.centerSnapRelease);
730
+ const tx = previous.centerX ? release : engage;
731
+ if (Math.abs(x - 0.5) <= tx + 1e-9) {
732
+ x = 0.5;
733
+ snapCenterX = true;
734
+ }
735
+ const ty = previous.centerY ? release : engage;
736
+ if (Math.abs(y - 0.5) <= ty + 1e-9) {
737
+ y = 0.5;
738
+ snapCenterY = true;
739
+ }
740
+ }
741
+ if (config.rotationSnap !== false && config.rotationEnabled) {
742
+ const engage = config.rotationSnap;
743
+ const release = Math.max(engage, config.rotationSnapRelease);
744
+ const step = Math.PI / 2;
745
+ const nearest = Math.round(rotation / step) * step;
746
+ const tr = previous.rotation ? release : engage;
747
+ if (Math.abs(rotation - nearest) <= tr + 1e-9) {
748
+ rotation = nearest;
749
+ snapRotation2 = true;
750
+ }
751
+ }
752
+ return { x, y, scale, rotation, snapCenterX, snapCenterY, snapRotation: snapRotation2 };
753
+ }
754
+ function isOverDeleteTargetUI(x, y, target, aspect) {
755
+ "worklet";
756
+ const dx = x - target.x;
757
+ const dy = (y - target.y) * aspect;
758
+ return Math.sqrt(dx * dx + dy * dy) <= target.radius;
759
+ }
760
+ function layerPreviewDeltaUI(committed, live, width, height) {
761
+ "worklet";
762
+ return [
763
+ { translateX: live.x * width },
764
+ { translateY: live.y * height },
765
+ { rotate: live.rotation - committed.rotation },
766
+ { scale: live.scale / Math.max(committed.scale, 1e-9) },
767
+ { translateX: -committed.x * width },
768
+ { translateY: -committed.y * height }
769
+ ];
770
+ }
771
+ function hitTestLayerCandidatesUI(x, y, candidates, documentWidth, documentHeight) {
772
+ "worklet";
773
+ for (let i = 0; i < candidates.length; i++) {
774
+ const candidate = candidates[i];
775
+ let dx = x * documentWidth - candidate.x * documentWidth;
776
+ let dy = y * documentHeight - candidate.y * documentHeight;
777
+ const cos = Math.cos(-candidate.rotation);
778
+ const sin = Math.sin(-candidate.rotation);
779
+ const rx = dx * cos - dy * sin;
780
+ const ry = dx * sin + dy * cos;
781
+ dx = candidate.flipX ? -rx : rx;
782
+ dy = candidate.flipY ? -ry : ry;
783
+ const width = candidate.scale * candidate.width * documentWidth;
784
+ const height = candidate.scale * candidate.height * documentWidth;
785
+ if (width <= 0 || height <= 0) continue;
786
+ const localX = dx / width + candidate.anchorX;
787
+ const localY = dy / height + candidate.anchorY;
788
+ if (localX >= 0 && localX <= 1 && localY >= 0 && localY <= 1) return candidate;
789
+ }
790
+ return null;
791
+ }
792
+
793
+ // src/gestures/LayerGestureSurface.tsx
794
+ var import_jsx_runtime8 = require("react/jsx-runtime");
795
+ function LayerGestureSurface({ onSnap }) {
796
+ const { document, dispatch, profile } = (0, import_core8.useEditor)();
797
+ const store = (0, import_core8.useEditorStore)();
798
+ const metrics = (0, import_skia4.useCanvasMetrics)();
799
+ const { setPreview, setLayerDragActive, setOverDeleteTarget, setSnap } = useInteractionState();
800
+ const renderer = (0, import_skia4.useRenderer)();
801
+ const config = (0, import_react9.useMemo)(
802
+ () => (0, import_core8.layerGestureConfigFromProfile)(profile.interaction?.layers),
803
+ [profile]
804
+ );
805
+ const deleteTarget = (0, import_react9.useMemo)(
806
+ () => (0, import_core8.deleteTargetFromProfile)(profile.interaction?.layers),
807
+ [profile]
808
+ );
809
+ const aspect = document.size.height / document.size.width;
810
+ const [measureVersion, setMeasureVersion] = (0, import_react9.useState)(0);
811
+ (0, import_react9.useEffect)(
812
+ () => renderer.textMeasures.subscribe(() => setMeasureVersion((v) => v + 1)),
813
+ [renderer]
814
+ );
815
+ const hitCandidates = (0, import_react9.useMemo)(
816
+ () => document.layers.map((layer, index) => ({ layer, index })).filter(
817
+ ({ layer, index }) => !layer.hidden && !layer.locked && (0, import_core8.isDirectManipulationLayer)(layer, index, profile.interaction?.layers)
818
+ ).reverse().map(({ layer }) => {
819
+ const natural = (0, import_skia4.preferMeasuredSize)(
820
+ renderer.textMeasures.get(layer.id),
821
+ (0, import_core8.layerNaturalSize)(layer, store.registry, document)
822
+ );
823
+ const anchor = layer.transform.anchor ?? { x: 0.5, y: 0.5 };
824
+ return {
825
+ id: layer.id,
826
+ x: layer.transform.x,
827
+ y: layer.transform.y,
828
+ scale: layer.transform.scale,
829
+ rotation: layer.transform.rotation,
830
+ width: natural.width,
831
+ height: natural.height,
832
+ anchorX: anchor.x,
833
+ anchorY: anchor.y,
834
+ flipX: layer.transform.flipX ?? false,
835
+ flipY: layer.transform.flipY ?? false
836
+ };
837
+ }),
838
+ // `measureVersion` is deliberate: the measurements live outside React, so
839
+ // this is how a new one re-derives the hit geometry.
840
+ // eslint-disable-next-line react-hooks/exhaustive-deps
841
+ [document, profile, store.registry, renderer, measureVersion]
842
+ );
843
+ const liveX = (0, import_react_native_reanimated4.useSharedValue)(0.5);
844
+ const liveY = (0, import_react_native_reanimated4.useSharedValue)(0.5);
845
+ const liveScale = (0, import_react_native_reanimated4.useSharedValue)(1);
846
+ const liveRotation = (0, import_react_native_reanimated4.useSharedValue)(0);
847
+ const committedX = (0, import_react_native_reanimated4.useSharedValue)(0.5);
848
+ const committedY = (0, import_react_native_reanimated4.useSharedValue)(0.5);
849
+ const committedScale = (0, import_react_native_reanimated4.useSharedValue)(1);
850
+ const committedRotation = (0, import_react_native_reanimated4.useSharedValue)(0);
851
+ const baseX = (0, import_react_native_reanimated4.useSharedValue)(0.5);
852
+ const baseY = (0, import_react_native_reanimated4.useSharedValue)(0.5);
853
+ const baseScale = (0, import_react_native_reanimated4.useSharedValue)(1);
854
+ const baseRotation = (0, import_react_native_reanimated4.useSharedValue)(0);
855
+ const originPanX = (0, import_react_native_reanimated4.useSharedValue)(0);
856
+ const originPanY = (0, import_react_native_reanimated4.useSharedValue)(0);
857
+ const originPinch = (0, import_react_native_reanimated4.useSharedValue)(1);
858
+ const originRotate = (0, import_react_native_reanimated4.useSharedValue)(0);
859
+ const rawPanX = (0, import_react_native_reanimated4.useSharedValue)(0);
860
+ const rawPanY = (0, import_react_native_reanimated4.useSharedValue)(0);
861
+ const rawPinch = (0, import_react_native_reanimated4.useSharedValue)(1);
862
+ const rawRotate = (0, import_react_native_reanimated4.useSharedValue)(0);
863
+ const focalX = (0, import_react_native_reanimated4.useSharedValue)(0.5);
864
+ const focalY = (0, import_react_native_reanimated4.useSharedValue)(0.5);
865
+ const pointers = (0, import_react_native_reanimated4.useSharedValue)(0);
866
+ const active = (0, import_react_native_reanimated4.useSharedValue)(false);
867
+ const snapCX = (0, import_react_native_reanimated4.useSharedValue)(false);
868
+ const snapCY = (0, import_react_native_reanimated4.useSharedValue)(false);
869
+ const snapR = (0, import_react_native_reanimated4.useSharedValue)(false);
870
+ const overDelete = (0, import_react_native_reanimated4.useSharedValue)(false);
871
+ const previewTransform = (0, import_react_native_reanimated4.useDerivedValue)(() => {
872
+ return layerPreviewDeltaUI(
873
+ {
874
+ x: committedX.value,
875
+ y: committedY.value,
876
+ scale: committedScale.value,
877
+ rotation: committedRotation.value
878
+ },
879
+ { x: liveX.value, y: liveY.value, scale: liveScale.value, rotation: liveRotation.value },
880
+ document.size.width,
881
+ document.size.height
882
+ );
883
+ }, [document.size.width, document.size.height]);
884
+ const latest = (0, import_react9.useRef)({ document, metrics, store, dispatch, onSnap, profile });
885
+ latest.current = { document, metrics, store, dispatch, onSnap, profile };
886
+ const grabbedId = (0, import_react9.useRef)(null);
887
+ const grabbedFrom = (0, import_react9.useRef)(null);
888
+ const snapState = (0, import_react9.useRef)(import_core8.NO_SNAP);
889
+ const onGrabLayer = (0, import_react9.useCallback)((id) => {
890
+ const { document: doc, store: s } = latest.current;
891
+ const hit = doc.layers.find((layer) => layer.id === id);
892
+ if (!hit) return;
893
+ const t = hit.transform;
894
+ grabbedId.current = hit.id;
895
+ grabbedFrom.current = { ...t };
896
+ s.select([hit.id]);
897
+ setPreview({ layerId: hit.id, transform: previewTransform });
898
+ setLayerDragActive(true);
899
+ }, [setPreview, setLayerDragActive, previewTransform]);
900
+ const onSelectAt = (0, import_react9.useCallback)((xDp, yDp) => {
901
+ const { document: doc, metrics: m, store: s } = latest.current;
902
+ if (!m) return;
903
+ const layerInteraction = latest.current.profile.interaction?.layers;
904
+ const hit = (0, import_core8.hitTestDocument)(
905
+ doc,
906
+ { x: xDp / m.width, y: yDp / m.height },
907
+ s.registry,
908
+ (layer, index) => (0, import_core8.isDirectManipulationLayer)(layer, index, layerInteraction)
909
+ );
910
+ s.select(hit && !hit.locked ? [hit.id] : []);
911
+ }, []);
912
+ const onSnapChanged = (0, import_react9.useCallback)(
913
+ (centerX, centerY, rotation) => {
914
+ const previous = snapState.current;
915
+ const next = { centerX, centerY, rotation };
916
+ snapState.current = next;
917
+ const notify = latest.current.onSnap;
918
+ if (centerX && !previous.centerX) notify?.({ kind: "centerX" });
919
+ if (centerY && !previous.centerY) notify?.({ kind: "centerY" });
920
+ if (rotation && !previous.rotation) notify?.({ kind: "rotation" });
921
+ setSnap(next);
922
+ },
923
+ [setSnap]
924
+ );
925
+ const onHoverChanged = (0, import_react9.useCallback)(
926
+ (over) => setOverDeleteTarget(over),
927
+ [setOverDeleteTarget]
928
+ );
929
+ const onGestureFinished = (0, import_react9.useCallback)(
930
+ (t, deleted) => {
931
+ const { dispatch: d, store: s } = latest.current;
932
+ const id = grabbedId.current;
933
+ const from = grabbedFrom.current;
934
+ grabbedId.current = null;
935
+ grabbedFrom.current = null;
936
+ snapState.current = import_core8.NO_SNAP;
937
+ setLayerDragActive(false);
938
+ setOverDeleteTarget(false);
939
+ setSnap(import_core8.NO_SNAP);
940
+ setPreview(null);
941
+ if (!id) return;
942
+ if (deleted) {
943
+ d({ type: "layer/remove", id });
944
+ s.select([]);
945
+ return;
946
+ }
947
+ if (from && (0, import_core8.isSameTransform)(from, t)) return;
948
+ d({ type: "layer/transform", id, patch: t });
949
+ },
950
+ [setLayerDragActive, setOverDeleteTarget, setSnap, setPreview]
951
+ );
952
+ (0, import_react9.useEffect)(() => () => setPreview(null), [setPreview]);
953
+ const gesture = (0, import_react9.useMemo)(() => {
954
+ const metricsWidth = metrics?.width ?? 0;
955
+ const metricsHeight = metrics?.height ?? 0;
956
+ const target = deleteTarget;
957
+ const candidates = hitCandidates;
958
+ const rebase = () => {
959
+ "worklet";
960
+ baseX.value = liveX.value;
961
+ baseY.value = liveY.value;
962
+ baseScale.value = liveScale.value;
963
+ baseRotation.value = liveRotation.value;
964
+ originPanX.value = rawPanX.value;
965
+ originPanY.value = rawPanY.value;
966
+ originPinch.value = rawPinch.value;
967
+ originRotate.value = rawRotate.value;
968
+ };
969
+ const recompute = (settle) => {
970
+ "worklet";
971
+ if (!active.value || metricsWidth === 0 || metricsHeight === 0) return;
972
+ const out = composeLayerGestureUI(
973
+ {
974
+ x: baseX.value,
975
+ y: baseY.value,
976
+ scale: baseScale.value,
977
+ rotation: baseRotation.value
978
+ },
979
+ {
980
+ panX: (rawPanX.value - originPanX.value) / metricsWidth,
981
+ panY: (rawPanY.value - originPanY.value) / metricsHeight,
982
+ pinch: rawPinch.value / Math.max(originPinch.value, 1e-9),
983
+ rotate: rawRotate.value - originRotate.value,
984
+ focalX: focalX.value,
985
+ focalY: focalY.value
986
+ },
987
+ aspect,
988
+ config,
989
+ { centerX: snapCX.value, centerY: snapCY.value, rotation: snapR.value },
990
+ settle
991
+ );
992
+ liveX.value = out.x;
993
+ liveY.value = out.y;
994
+ liveScale.value = out.scale;
995
+ liveRotation.value = out.rotation;
996
+ if (out.snapCenterX !== snapCX.value || out.snapCenterY !== snapCY.value || out.snapRotation !== snapR.value) {
997
+ snapCX.value = out.snapCenterX;
998
+ snapCY.value = out.snapCenterY;
999
+ snapR.value = out.snapRotation;
1000
+ (0, import_react_native_worklets4.runOnJS)(onSnapChanged)(out.snapCenterX, out.snapCenterY, out.snapRotation);
1001
+ }
1002
+ if (target) {
1003
+ const over = isOverDeleteTargetUI(out.x, out.y, target, aspect);
1004
+ if (over !== overDelete.value) {
1005
+ overDelete.value = over;
1006
+ (0, import_react_native_worklets4.runOnJS)(onHoverChanged)(over);
1007
+ }
1008
+ }
1009
+ };
1010
+ const syncPointers = (count) => {
1011
+ "worklet";
1012
+ if (active.value && count !== pointers.value) {
1013
+ pointers.value = count;
1014
+ rebase();
1015
+ }
1016
+ };
1017
+ const endIfIdle = () => {
1018
+ "worklet";
1019
+ if (!active.value) {
1020
+ (0, import_react_native_worklets4.runOnJS)(onGestureFinished)({ x: 0, y: 0, scale: 1, rotation: 0 }, false);
1021
+ return;
1022
+ }
1023
+ recompute(true);
1024
+ active.value = false;
1025
+ pointers.value = 0;
1026
+ const deleted = overDelete.value;
1027
+ overDelete.value = false;
1028
+ snapCX.value = false;
1029
+ snapCY.value = false;
1030
+ snapR.value = false;
1031
+ (0, import_react_native_worklets4.runOnJS)(onGestureFinished)(
1032
+ {
1033
+ x: liveX.value,
1034
+ y: liveY.value,
1035
+ scale: liveScale.value,
1036
+ rotation: liveRotation.value
1037
+ },
1038
+ deleted
1039
+ );
1040
+ };
1041
+ const pan = import_react_native_gesture_handler4.Gesture.Pan().minPointers(1).maxPointers(2).onBegin((e) => {
1042
+ "worklet";
1043
+ if (active.value) return;
1044
+ rawPanX.value = 0;
1045
+ rawPanY.value = 0;
1046
+ rawPinch.value = 1;
1047
+ rawRotate.value = 0;
1048
+ originPanX.value = 0;
1049
+ originPanY.value = 0;
1050
+ originPinch.value = 1;
1051
+ originRotate.value = 0;
1052
+ pointers.value = e.numberOfPointers;
1053
+ const hit = hitTestLayerCandidatesUI(
1054
+ e.x / Math.max(metricsWidth, 1e-9),
1055
+ e.y / Math.max(metricsHeight, 1e-9),
1056
+ candidates,
1057
+ document.size.width,
1058
+ document.size.height
1059
+ );
1060
+ if (!hit) {
1061
+ active.value = false;
1062
+ return;
1063
+ }
1064
+ liveX.value = hit.x;
1065
+ liveY.value = hit.y;
1066
+ liveScale.value = hit.scale;
1067
+ liveRotation.value = hit.rotation;
1068
+ committedX.value = hit.x;
1069
+ committedY.value = hit.y;
1070
+ committedScale.value = hit.scale;
1071
+ committedRotation.value = hit.rotation;
1072
+ baseX.value = hit.x;
1073
+ baseY.value = hit.y;
1074
+ baseScale.value = hit.scale;
1075
+ baseRotation.value = hit.rotation;
1076
+ focalX.value = hit.x;
1077
+ focalY.value = hit.y;
1078
+ active.value = true;
1079
+ (0, import_react_native_worklets4.runOnJS)(onGrabLayer)(hit.id);
1080
+ }).onUpdate((e) => {
1081
+ "worklet";
1082
+ rawPanX.value = e.translationX;
1083
+ rawPanY.value = e.translationY;
1084
+ syncPointers(e.numberOfPointers);
1085
+ recompute(false);
1086
+ }).onFinalize(() => {
1087
+ "worklet";
1088
+ endIfIdle();
1089
+ });
1090
+ const pinch = import_react_native_gesture_handler4.Gesture.Pinch().onUpdate((e) => {
1091
+ "worklet";
1092
+ rawPinch.value = e.scale;
1093
+ focalX.value = e.focalX / metricsWidth;
1094
+ focalY.value = e.focalY / metricsHeight;
1095
+ syncPointers(e.numberOfPointers);
1096
+ recompute(false);
1097
+ });
1098
+ const rotate = import_react_native_gesture_handler4.Gesture.Rotation().enabled(config.rotationEnabled).onUpdate((e) => {
1099
+ "worklet";
1100
+ rawRotate.value = e.rotation;
1101
+ focalX.value = e.anchorX / metricsWidth;
1102
+ focalY.value = e.anchorY / metricsHeight;
1103
+ syncPointers(e.numberOfPointers);
1104
+ recompute(false);
1105
+ });
1106
+ const tap = import_react_native_gesture_handler4.Gesture.Tap().onEnd((e) => {
1107
+ "worklet";
1108
+ (0, import_react_native_worklets4.runOnJS)(onSelectAt)(e.x, e.y);
1109
+ });
1110
+ return import_react_native_gesture_handler4.Gesture.Simultaneous(import_react_native_gesture_handler4.Gesture.Race(pan, tap), pinch, rotate);
1111
+ }, [
1112
+ metrics,
1113
+ config,
1114
+ deleteTarget,
1115
+ aspect,
1116
+ onGrabLayer,
1117
+ onSelectAt,
1118
+ onSnapChanged,
1119
+ onHoverChanged,
1120
+ onGestureFinished,
1121
+ hitCandidates,
1122
+ document.size.width,
1123
+ document.size.height
1124
+ ]);
1125
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_gesture_handler4.GestureDetector, { gesture, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_reanimated4.default.View, { style: import_react_native5.StyleSheet.absoluteFill }) });
1126
+ }
1127
+
1128
+ // src/gestures/StoryMediaGestureSurface.tsx
1129
+ var import_core10 = require("@pictor-sdk/core");
1130
+ var import_skia5 = require("@pictor-sdk/skia");
1131
+ var import_react10 = require("react");
1132
+ var import_react_native6 = require("react-native");
1133
+ var import_react_native_gesture_handler5 = require("react-native-gesture-handler");
1134
+ var import_react_native_reanimated5 = __toESM(require("react-native-reanimated"));
1135
+ var import_react_native_worklets5 = require("react-native-worklets");
1136
+
1137
+ // src/gestures/mediaGestureWorklet.ts
1138
+ function mediaFitScaleUI(intrinsic, canvas, fit, rotation) {
1139
+ "worklet";
1140
+ if (intrinsic.width <= 0 || intrinsic.height <= 0) return 1;
1141
+ if (fit === "width") return 1;
1142
+ const w = canvas.width;
1143
+ const h = canvas.width * (intrinsic.height / intrinsic.width);
1144
+ const cos = Math.abs(Math.cos(rotation));
1145
+ const sin = Math.abs(Math.sin(rotation));
1146
+ const byWidth = canvas.width / (w * cos + h * sin);
1147
+ const byHeight = canvas.height / (w * sin + h * cos);
1148
+ return fit === "cover" ? Math.max(byWidth, byHeight) : Math.min(byWidth, byHeight);
1149
+ }
1150
+ function mediaPanBoundsUI(_intrinsic, _canvas, _scale, _rotation) {
1151
+ "worklet";
1152
+ return { minX: 0, maxX: 1, minY: 0, maxY: 1 };
1153
+ }
1154
+ function composeMediaGestureUI(base, input, canvas, intrinsic, config, settle) {
1155
+ "worklet";
1156
+ const rotationDelta = config.rotationEnabled ? input.rotate : 0;
1157
+ const rotation = base.rotation + rotationDelta;
1158
+ const fit = mediaFitScaleUI(intrinsic, canvas, config.fit, rotation);
1159
+ const minScale = fit * config.minScaleFactor;
1160
+ const maxScale = Math.max(minScale, config.maxScale);
1161
+ const requested = base.scale * input.pinch;
1162
+ let scale;
1163
+ if (settle) {
1164
+ scale = Math.min(maxScale, Math.max(minScale, requested));
1165
+ } else if (requested < minScale) {
1166
+ scale = minScale - (minScale - requested) * config.resistance;
1167
+ } else if (requested > maxScale) {
1168
+ scale = maxScale + (requested - maxScale) * config.resistance;
1169
+ } else {
1170
+ scale = requested;
1171
+ }
1172
+ const aspect = canvas.height / canvas.width;
1173
+ const ratio = scale / Math.max(base.scale, 1e-9);
1174
+ const dx = base.x - input.focalX;
1175
+ const dy = (base.y - input.focalY) * aspect;
1176
+ const cos = Math.cos(rotationDelta);
1177
+ const sin = Math.sin(rotationDelta);
1178
+ const rx = (dx * cos - dy * sin) * ratio;
1179
+ const ry = (dx * sin + dy * cos) * ratio;
1180
+ const rawX = input.focalX + rx + input.panX;
1181
+ const rawY = input.focalY + ry / aspect + input.panY;
1182
+ const bounds = mediaPanBoundsUI(intrinsic, canvas, scale, rotation);
1183
+ let x;
1184
+ let y;
1185
+ if (settle) {
1186
+ x = Math.min(bounds.maxX, Math.max(bounds.minX, rawX));
1187
+ y = Math.min(bounds.maxY, Math.max(bounds.minY, rawY));
1188
+ } else {
1189
+ x = rawX < bounds.minX ? bounds.minX - (bounds.minX - rawX) * config.resistance : rawX > bounds.maxX ? bounds.maxX + (rawX - bounds.maxX) * config.resistance : rawX;
1190
+ y = rawY < bounds.minY ? bounds.minY - (bounds.minY - rawY) * config.resistance : rawY > bounds.maxY ? bounds.maxY + (rawY - bounds.maxY) * config.resistance : rawY;
1191
+ }
1192
+ return { x, y, scale, rotation };
1193
+ }
1194
+ function onRecognizerBegin(count) {
1195
+ "worklet";
1196
+ const next = Math.max(0, count) + 1;
1197
+ return { count: next, isFirst: next === 1 };
1198
+ }
1199
+ function onRecognizerEnd(count) {
1200
+ "worklet";
1201
+ const next = Math.max(0, count - 1);
1202
+ return { count: next, isLast: next === 0 && count > 0 };
1203
+ }
1204
+
1205
+ // src/gestures/settleCoordinator.ts
1206
+ function createSettleCoordinator(commit) {
1207
+ let generation = 0;
1208
+ let settling = false;
1209
+ return {
1210
+ settle(target, animate) {
1211
+ generation += 1;
1212
+ const mine = generation;
1213
+ settling = true;
1214
+ animate((finished) => {
1215
+ if (mine !== generation) return;
1216
+ generation += 1;
1217
+ settling = false;
1218
+ if (finished) commit(target);
1219
+ });
1220
+ },
1221
+ cancel() {
1222
+ generation += 1;
1223
+ settling = false;
1224
+ },
1225
+ isSettling() {
1226
+ return settling;
1227
+ }
1228
+ };
1229
+ }
1230
+
1231
+ // src/gestures/transformOwnership.ts
1232
+ var import_core9 = require("@pictor-sdk/core");
1233
+ var IDLE_OWNERSHIP = { owning: false, pending: null, previous: null };
1234
+ function beginOwnership() {
1235
+ return { owning: true, pending: null, previous: null };
1236
+ }
1237
+ function commitOwnership(target, dispatched, previous) {
1238
+ return dispatched ? { owning: true, pending: target, previous: previous ?? null } : IDLE_OWNERSHIP;
1239
+ }
1240
+ function documentChanged(state, committed) {
1241
+ if (!committed) return { state, reseed: false };
1242
+ if (state.pending) {
1243
+ if ((0, import_core9.isSameTransform)(state.pending, committed)) return { state: IDLE_OWNERSHIP, reseed: false };
1244
+ if (state.previous && (0, import_core9.isSameTransform)(state.previous, committed)) {
1245
+ return { state, reseed: false };
1246
+ }
1247
+ return { state: IDLE_OWNERSHIP, reseed: true };
1248
+ }
1249
+ if (state.owning) return { state, reseed: false };
1250
+ return { state, reseed: true };
1251
+ }
1252
+
1253
+ // src/gestures/StoryMediaGestureSurface.tsx
1254
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1255
+ function useBaseMediaLayer() {
1256
+ const { document } = (0, import_core10.useEditor)();
1257
+ const base = document.layers[0];
1258
+ return base && (0, import_core10.isBaseImageLayer)(document.layers, base.id) ? base : void 0;
1259
+ }
1260
+ function StoryMediaGestureSurface({
1261
+ onInteractionChange
1262
+ }) {
1263
+ const { document, dispatch, profile } = (0, import_core10.useEditor)();
1264
+ const metrics = (0, import_skia5.useCanvasMetrics)();
1265
+ const { setPreview } = useInteractionState();
1266
+ const layer = useBaseMediaLayer();
1267
+ const config = (0, import_react10.useMemo)(
1268
+ () => (0, import_core10.mediaConfigFromProfile)(profile.interaction?.media),
1269
+ [profile]
1270
+ );
1271
+ const declared = layer?.data?.intrinsic;
1272
+ const resolved = (0, import_skia5.useResolvedImageSize)(layer?.data?.source);
1273
+ const intrinsic = (0, import_react10.useMemo)(
1274
+ () => (0, import_skia5.preferResolvedSize)(resolved, declared),
1275
+ [resolved, declared]
1276
+ );
1277
+ const canvas = document.size;
1278
+ const liveX = (0, import_react_native_reanimated5.useSharedValue)(0.5);
1279
+ const liveY = (0, import_react_native_reanimated5.useSharedValue)(0.5);
1280
+ const liveScale = (0, import_react_native_reanimated5.useSharedValue)(1);
1281
+ const liveRotation = (0, import_react_native_reanimated5.useSharedValue)(0);
1282
+ const baseX = (0, import_react_native_reanimated5.useSharedValue)(0.5);
1283
+ const baseY = (0, import_react_native_reanimated5.useSharedValue)(0.5);
1284
+ const baseScale = (0, import_react_native_reanimated5.useSharedValue)(1);
1285
+ const baseRotation = (0, import_react_native_reanimated5.useSharedValue)(0);
1286
+ const originPanX = (0, import_react_native_reanimated5.useSharedValue)(0);
1287
+ const originPanY = (0, import_react_native_reanimated5.useSharedValue)(0);
1288
+ const originPinch = (0, import_react_native_reanimated5.useSharedValue)(1);
1289
+ const originRotate = (0, import_react_native_reanimated5.useSharedValue)(0);
1290
+ const rawPanX = (0, import_react_native_reanimated5.useSharedValue)(0);
1291
+ const rawPanY = (0, import_react_native_reanimated5.useSharedValue)(0);
1292
+ const rawPinch = (0, import_react_native_reanimated5.useSharedValue)(1);
1293
+ const rawRotate = (0, import_react_native_reanimated5.useSharedValue)(0);
1294
+ const focalX = (0, import_react_native_reanimated5.useSharedValue)(0.5);
1295
+ const focalY = (0, import_react_native_reanimated5.useSharedValue)(0.5);
1296
+ const active = (0, import_react_native_reanimated5.useSharedValue)(false);
1297
+ const recognizers = (0, import_react_native_reanimated5.useSharedValue)(0);
1298
+ const startedPan = (0, import_react_native_reanimated5.useSharedValue)(false);
1299
+ const startedPinch = (0, import_react_native_reanimated5.useSharedValue)(false);
1300
+ const startedRotate = (0, import_react_native_reanimated5.useSharedValue)(false);
1301
+ const settleProgress = (0, import_react_native_reanimated5.useSharedValue)(0);
1302
+ const committed = layer?.transform;
1303
+ const ownership = (0, import_react10.useRef)(IDLE_OWNERSHIP);
1304
+ (0, import_react10.useEffect)(() => {
1305
+ const next = documentChanged(ownership.current, committed);
1306
+ ownership.current = next.state;
1307
+ if (!next.reseed || !committed) return;
1308
+ liveX.value = committed.x;
1309
+ liveY.value = committed.y;
1310
+ liveScale.value = committed.scale;
1311
+ liveRotation.value = committed.rotation;
1312
+ }, [committed?.x, committed?.y, committed?.scale, committed?.rotation]);
1313
+ const baseCommittedX = (0, import_react_native_reanimated5.useSharedValue)(0.5);
1314
+ const baseCommittedY = (0, import_react_native_reanimated5.useSharedValue)(0.5);
1315
+ const baseCommittedScale = (0, import_react_native_reanimated5.useSharedValue)(1);
1316
+ const baseCommittedRotation = (0, import_react_native_reanimated5.useSharedValue)(0);
1317
+ const previewTransform = (0, import_react_native_reanimated5.useDerivedValue)(() => {
1318
+ const ratio = liveScale.value / Math.max(baseCommittedScale.value, 1e-9);
1319
+ return [
1320
+ { translateX: liveX.value * canvas.width },
1321
+ { translateY: liveY.value * canvas.height },
1322
+ { rotate: liveRotation.value - baseCommittedRotation.value },
1323
+ { scale: ratio },
1324
+ { translateX: -baseCommittedX.value * canvas.width },
1325
+ { translateY: -baseCommittedY.value * canvas.height }
1326
+ ];
1327
+ }, [canvas.width, canvas.height]);
1328
+ (0, import_react10.useEffect)(() => () => setPreview(null), [setPreview]);
1329
+ const latest = (0, import_react10.useRef)({ layer, dispatch, onInteractionChange, previewTransform });
1330
+ latest.current = { layer, dispatch, onInteractionChange, previewTransform };
1331
+ const settleCoordinatorRef = (0, import_react10.useRef)(null);
1332
+ const beginInteraction = (0, import_react10.useCallback)(() => {
1333
+ settleCoordinatorRef.current?.cancel();
1334
+ ownership.current = beginOwnership();
1335
+ const current = latest.current.layer;
1336
+ if (current) {
1337
+ baseCommittedX.value = current.transform.x;
1338
+ baseCommittedY.value = current.transform.y;
1339
+ baseCommittedScale.value = current.transform.scale;
1340
+ baseCommittedRotation.value = current.transform.rotation;
1341
+ setPreview({ layerId: current.id, transform: latest.current.previewTransform });
1342
+ }
1343
+ latest.current.onInteractionChange?.(true);
1344
+ }, [setPreview]);
1345
+ const commitNow = (0, import_react10.useCallback)((next) => {
1346
+ const current = latest.current.layer;
1347
+ setPreview(null);
1348
+ latest.current.onInteractionChange?.(false);
1349
+ const dispatched = current !== void 0 && !(0, import_core10.isSameTransform)(current.transform, next);
1350
+ ownership.current = commitOwnership(next, dispatched, current?.transform);
1351
+ if (!dispatched || !current) return;
1352
+ latest.current.dispatch({
1353
+ type: "layer/transform",
1354
+ id: current.id,
1355
+ patch: { x: next.x, y: next.y, scale: next.scale, rotation: next.rotation }
1356
+ });
1357
+ }, [setPreview]);
1358
+ const settleCoordinator = (0, import_react10.useMemo)(
1359
+ () => createSettleCoordinator((target) => commitNow(target)),
1360
+ // commitNow reads through `latest`, so it is stable
1361
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1362
+ []
1363
+ );
1364
+ settleCoordinatorRef.current = settleCoordinator;
1365
+ const settleTo = (0, import_react10.useCallback)(
1366
+ (target, duration) => {
1367
+ settleCoordinator.settle(target, (done) => {
1368
+ liveX.value = (0, import_react_native_reanimated5.withTiming)(target.x, { duration });
1369
+ liveY.value = (0, import_react_native_reanimated5.withTiming)(target.y, { duration });
1370
+ liveRotation.value = (0, import_react_native_reanimated5.withTiming)(target.rotation, { duration });
1371
+ liveScale.value = (0, import_react_native_reanimated5.withTiming)(target.scale, { duration });
1372
+ settleProgress.value = 0;
1373
+ settleProgress.value = (0, import_react_native_reanimated5.withTiming)(1, { duration }, (finished) => {
1374
+ "worklet";
1375
+ (0, import_react_native_worklets5.runOnJS)(done)(finished === true);
1376
+ });
1377
+ });
1378
+ },
1379
+ // shared values are stable refs, so they are deliberately not dependencies
1380
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1381
+ [settleCoordinator]
1382
+ );
1383
+ (0, import_react10.useEffect)(
1384
+ () => () => {
1385
+ settleCoordinator.cancel();
1386
+ ownership.current = IDLE_OWNERSHIP;
1387
+ (0, import_react_native_reanimated5.cancelAnimation)(liveX);
1388
+ (0, import_react_native_reanimated5.cancelAnimation)(liveY);
1389
+ (0, import_react_native_reanimated5.cancelAnimation)(liveScale);
1390
+ (0, import_react_native_reanimated5.cancelAnimation)(liveRotation);
1391
+ (0, import_react_native_reanimated5.cancelAnimation)(settleProgress);
1392
+ },
1393
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1394
+ [settleCoordinator]
1395
+ );
1396
+ const runDoubleTap = (0, import_react10.useCallback)(
1397
+ (xDp, yDp) => {
1398
+ const current = latest.current.layer;
1399
+ if (!current || !metrics) return;
1400
+ const target = (0, import_core10.doubleTapMediaTransform)(current.transform, canvas, intrinsic, config, {
1401
+ x: xDp / metrics.width,
1402
+ y: yDp / metrics.height
1403
+ });
1404
+ beginInteraction();
1405
+ settleTo(target, config.settleMs);
1406
+ },
1407
+ [metrics, canvas, intrinsic, config, settleTo, beginInteraction]
1408
+ );
1409
+ const gesture = (0, import_react10.useMemo)(() => {
1410
+ const width = metrics?.width ?? 0;
1411
+ const height = metrics?.height ?? 0;
1412
+ const settleMs = config.settleMs;
1413
+ const recompute = () => {
1414
+ "worklet";
1415
+ if (!active.value || width === 0 || height === 0) return;
1416
+ const out = composeMediaGestureUI(
1417
+ {
1418
+ x: baseX.value,
1419
+ y: baseY.value,
1420
+ scale: baseScale.value,
1421
+ rotation: baseRotation.value
1422
+ },
1423
+ {
1424
+ panX: (rawPanX.value - originPanX.value) / width,
1425
+ panY: (rawPanY.value - originPanY.value) / height,
1426
+ pinch: rawPinch.value / Math.max(originPinch.value, 1e-9),
1427
+ rotate: rawRotate.value - originRotate.value,
1428
+ focalX: focalX.value,
1429
+ focalY: focalY.value
1430
+ },
1431
+ canvas,
1432
+ intrinsic,
1433
+ config,
1434
+ false
1435
+ );
1436
+ liveX.value = out.x;
1437
+ liveY.value = out.y;
1438
+ liveScale.value = out.scale;
1439
+ liveRotation.value = out.rotation;
1440
+ };
1441
+ const begin = () => {
1442
+ "worklet";
1443
+ const started = onRecognizerBegin(recognizers.value);
1444
+ recognizers.value = started.count;
1445
+ if (!started.isFirst) return;
1446
+ (0, import_react_native_reanimated5.cancelAnimation)(liveX);
1447
+ (0, import_react_native_reanimated5.cancelAnimation)(liveY);
1448
+ (0, import_react_native_reanimated5.cancelAnimation)(liveScale);
1449
+ (0, import_react_native_reanimated5.cancelAnimation)(liveRotation);
1450
+ (0, import_react_native_reanimated5.cancelAnimation)(settleProgress);
1451
+ active.value = true;
1452
+ rawPanX.value = 0;
1453
+ rawPanY.value = 0;
1454
+ rawPinch.value = 1;
1455
+ rawRotate.value = 0;
1456
+ originPanX.value = 0;
1457
+ originPanY.value = 0;
1458
+ originPinch.value = 1;
1459
+ originRotate.value = 0;
1460
+ baseX.value = liveX.value;
1461
+ baseY.value = liveY.value;
1462
+ baseScale.value = liveScale.value;
1463
+ baseRotation.value = liveRotation.value;
1464
+ focalX.value = liveX.value;
1465
+ focalY.value = liveY.value;
1466
+ (0, import_react_native_worklets5.runOnJS)(beginInteraction)();
1467
+ };
1468
+ const finish = () => {
1469
+ "worklet";
1470
+ const ended = onRecognizerEnd(recognizers.value);
1471
+ recognizers.value = ended.count;
1472
+ if (!ended.isLast || !active.value) return;
1473
+ active.value = false;
1474
+ const settled = composeMediaGestureUI(
1475
+ {
1476
+ x: baseX.value,
1477
+ y: baseY.value,
1478
+ scale: baseScale.value,
1479
+ rotation: baseRotation.value
1480
+ },
1481
+ {
1482
+ panX: (rawPanX.value - originPanX.value) / width,
1483
+ panY: (rawPanY.value - originPanY.value) / height,
1484
+ pinch: rawPinch.value / Math.max(originPinch.value, 1e-9),
1485
+ rotate: rawRotate.value - originRotate.value,
1486
+ focalX: focalX.value,
1487
+ focalY: focalY.value
1488
+ },
1489
+ canvas,
1490
+ intrinsic,
1491
+ config,
1492
+ true
1493
+ );
1494
+ (0, import_react_native_worklets5.runOnJS)(settleTo)(settled, settleMs);
1495
+ };
1496
+ const pan = import_react_native_gesture_handler5.Gesture.Pan().minPointers(1).maxPointers(2).onStart((e) => {
1497
+ "worklet";
1498
+ startedPan.value = true;
1499
+ begin();
1500
+ rawPanX.value = e.translationX;
1501
+ rawPanY.value = e.translationY;
1502
+ originPanX.value = e.translationX;
1503
+ originPanY.value = e.translationY;
1504
+ }).onUpdate((e) => {
1505
+ "worklet";
1506
+ rawPanX.value = e.translationX;
1507
+ rawPanY.value = e.translationY;
1508
+ recompute();
1509
+ }).onFinalize(() => {
1510
+ "worklet";
1511
+ if (!startedPan.value) return;
1512
+ startedPan.value = false;
1513
+ finish();
1514
+ });
1515
+ const pinch = import_react_native_gesture_handler5.Gesture.Pinch().onStart((e) => {
1516
+ "worklet";
1517
+ startedPinch.value = true;
1518
+ begin();
1519
+ rawPinch.value = e.scale;
1520
+ originPinch.value = e.scale;
1521
+ focalX.value = e.focalX / width;
1522
+ focalY.value = e.focalY / height;
1523
+ }).onUpdate((e) => {
1524
+ "worklet";
1525
+ rawPinch.value = e.scale;
1526
+ focalX.value = e.focalX / width;
1527
+ focalY.value = e.focalY / height;
1528
+ recompute();
1529
+ }).onFinalize(() => {
1530
+ "worklet";
1531
+ if (!startedPinch.value) return;
1532
+ startedPinch.value = false;
1533
+ finish();
1534
+ });
1535
+ const rotate = import_react_native_gesture_handler5.Gesture.Rotation().enabled(config.rotationEnabled).onStart((e) => {
1536
+ "worklet";
1537
+ startedRotate.value = true;
1538
+ begin();
1539
+ rawRotate.value = e.rotation;
1540
+ originRotate.value = e.rotation;
1541
+ }).onUpdate((e) => {
1542
+ "worklet";
1543
+ rawRotate.value = e.rotation;
1544
+ recompute();
1545
+ }).onFinalize(() => {
1546
+ "worklet";
1547
+ if (!startedRotate.value) return;
1548
+ startedRotate.value = false;
1549
+ finish();
1550
+ });
1551
+ const doubleTap = import_react_native_gesture_handler5.Gesture.Tap().numberOfTaps(2).onEnd((e) => {
1552
+ "worklet";
1553
+ (0, import_react_native_worklets5.runOnJS)(runDoubleTap)(e.x, e.y);
1554
+ });
1555
+ return import_react_native_gesture_handler5.Gesture.Simultaneous(import_react_native_gesture_handler5.Gesture.Race(pan, doubleTap), pinch, rotate);
1556
+ }, [metrics, canvas, intrinsic, config, settleTo, beginInteraction, runDoubleTap]);
1557
+ if (!layer) return null;
1558
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_gesture_handler5.GestureDetector, { gesture, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_reanimated5.default.View, { style: import_react_native6.StyleSheet.absoluteFill }) });
1559
+ }
1560
+
1561
+ // src/gestures/routing.ts
1562
+ function resolveGestureSurface(activeTool) {
1563
+ switch (activeTool) {
1564
+ case "text":
1565
+ return "text";
1566
+ case "media":
1567
+ return "media";
1568
+ case "draw":
1569
+ return "draw";
1570
+ case "crop":
1571
+ return "crop";
1572
+ case "tilt":
1573
+ return "tilt";
1574
+ default:
1575
+ return "overlay";
1576
+ }
1577
+ }
1578
+
1579
+ // src/gestures/InteractionGuides.tsx
1580
+ var import_core11 = require("@pictor-sdk/core");
1581
+ var import_skia6 = require("@pictor-sdk/skia");
1582
+ var import_react_native7 = require("react-native");
1583
+
1584
+ // src/theme.ts
1585
+ var import_react11 = require("react");
1586
+ var defaultTheme = {
1587
+ colors: {
1588
+ background: "#000000",
1589
+ surface: "#121212",
1590
+ text: "#ffffff",
1591
+ textMuted: "#9a9a9a",
1592
+ accent: "#3897f0",
1593
+ danger: "#ed4956",
1594
+ track: "#3a3a3a",
1595
+ thumb: "#ffffff"
1596
+ },
1597
+ radii: { sm: 6, md: 10, lg: 16 },
1598
+ spacing: { xs: 4, sm: 8, md: 14, lg: 22 },
1599
+ font: { size: 15, sizeSm: 12, weight: "600" }
1600
+ };
1601
+ function mergeTheme(overrides) {
1602
+ if (!overrides) return defaultTheme;
1603
+ return {
1604
+ colors: { ...defaultTheme.colors, ...overrides.colors },
1605
+ radii: { ...defaultTheme.radii, ...overrides.radii },
1606
+ spacing: { ...defaultTheme.spacing, ...overrides.spacing },
1607
+ font: { ...defaultTheme.font, ...overrides.font }
1608
+ };
1609
+ }
1610
+ var ThemeContext = (0, import_react11.createContext)(defaultTheme);
1611
+ function useTheme() {
1612
+ return (0, import_react11.useContext)(ThemeContext);
1613
+ }
1614
+
1615
+ // src/gestures/InteractionGuides.tsx
1616
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1617
+ function InteractionGuides({ deleteLabel = "Drag to delete" }) {
1618
+ const { profile } = (0, import_core11.useEditor)();
1619
+ const metrics = (0, import_skia6.useCanvasMetrics)();
1620
+ const theme = useTheme();
1621
+ const { snap, layerDragActive, overDeleteTarget } = useInteractionState();
1622
+ const target = (0, import_core11.deleteTargetFromProfile)(profile.interaction?.layers);
1623
+ if (!metrics) return null;
1624
+ const radius = target ? target.radius * metrics.width : 0;
1625
+ const size = radius * 2;
1626
+ const active = overDeleteTarget;
1627
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native7.View, { pointerEvents: "none", style: import_react_native7.StyleSheet.absoluteFill, children: [
1628
+ snap.centerX ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1629
+ import_react_native7.View,
1630
+ {
1631
+ style: {
1632
+ position: "absolute",
1633
+ left: metrics.width / 2 - 1,
1634
+ top: 0,
1635
+ width: 2,
1636
+ height: metrics.height,
1637
+ backgroundColor: theme.colors.accent
1638
+ }
1639
+ }
1640
+ ) : null,
1641
+ snap.centerY ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1642
+ import_react_native7.View,
1643
+ {
1644
+ style: {
1645
+ position: "absolute",
1646
+ top: metrics.height / 2 - 1,
1647
+ left: 0,
1648
+ height: 2,
1649
+ width: metrics.width,
1650
+ backgroundColor: theme.colors.accent
1651
+ }
1652
+ }
1653
+ ) : null,
1654
+ snap.rotation ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1655
+ import_react_native7.View,
1656
+ {
1657
+ style: {
1658
+ position: "absolute",
1659
+ top: theme.spacing.md,
1660
+ alignSelf: "center",
1661
+ backgroundColor: "rgba(0,0,0,0.6)",
1662
+ borderRadius: theme.radii.lg,
1663
+ paddingHorizontal: theme.spacing.md,
1664
+ paddingVertical: theme.spacing.xs
1665
+ },
1666
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1667
+ import_react_native7.Text,
1668
+ {
1669
+ style: {
1670
+ color: theme.colors.text,
1671
+ fontSize: theme.font.sizeSm,
1672
+ fontWeight: theme.font.weight
1673
+ },
1674
+ children: "Right-angle snap"
1675
+ }
1676
+ )
1677
+ }
1678
+ ) : null,
1679
+ target && layerDragActive ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1680
+ import_react_native7.View,
1681
+ {
1682
+ style: {
1683
+ position: "absolute",
1684
+ left: target.x * metrics.width - radius,
1685
+ top: target.y * metrics.height - radius,
1686
+ width: size,
1687
+ height: size,
1688
+ alignItems: "center",
1689
+ justifyContent: "center"
1690
+ },
1691
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1692
+ import_react_native7.View,
1693
+ {
1694
+ style: {
1695
+ width: size,
1696
+ height: size,
1697
+ borderRadius: radius,
1698
+ borderWidth: 2,
1699
+ borderColor: theme.colors.text,
1700
+ // a scrim keeps the target legible over light photos, which a
1701
+ // plain white ring is not
1702
+ backgroundColor: active ? theme.colors.danger : "rgba(0,0,0,0.45)",
1703
+ alignItems: "center",
1704
+ justifyContent: "center",
1705
+ transform: [{ scale: active ? 1.15 : 1 }]
1706
+ },
1707
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1708
+ import_react_native7.Text,
1709
+ {
1710
+ style: {
1711
+ color: theme.colors.text,
1712
+ fontSize: radius * 0.7,
1713
+ fontWeight: theme.font.weight
1714
+ },
1715
+ children: "\xD7"
1716
+ }
1717
+ )
1718
+ }
1719
+ )
1720
+ }
1721
+ ) : null,
1722
+ target && layerDragActive ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1723
+ import_react_native7.View,
1724
+ {
1725
+ style: {
1726
+ position: "absolute",
1727
+ left: 0,
1728
+ right: 0,
1729
+ top: target.y * metrics.height - radius - theme.spacing.lg * 2,
1730
+ alignItems: "center"
1731
+ },
1732
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1733
+ import_react_native7.Text,
1734
+ {
1735
+ style: {
1736
+ color: theme.colors.text,
1737
+ fontSize: theme.font.sizeSm,
1738
+ fontWeight: theme.font.weight,
1739
+ backgroundColor: "rgba(0,0,0,0.45)",
1740
+ paddingHorizontal: theme.spacing.sm,
1741
+ paddingVertical: theme.spacing.xs,
1742
+ borderRadius: theme.radii.sm,
1743
+ overflow: "hidden"
1744
+ },
1745
+ numberOfLines: 1,
1746
+ children: deleteLabel
1747
+ }
1748
+ )
1749
+ }
1750
+ ) : null
1751
+ ] });
1752
+ }
1753
+
1754
+ // src/ToolPanelHost.tsx
1755
+ var import_core25 = require("@pictor-sdk/core");
1756
+ var import_react19 = require("react");
1757
+ var import_react_native23 = require("react-native");
1758
+
1759
+ // src/panels/AdjustPanel.tsx
1760
+ var import_core12 = require("@pictor-sdk/core");
1761
+ var import_react13 = require("react");
1762
+ var import_react_native9 = require("react-native");
1763
+
1764
+ // src/components/Slider.tsx
1765
+ var import_react12 = require("react");
1766
+ var import_react_native8 = require("react-native");
1767
+ var import_react_native_gesture_handler6 = require("react-native-gesture-handler");
1768
+ var import_react_native_reanimated6 = __toESM(require("react-native-reanimated"));
1769
+ var import_react_native_worklets6 = require("react-native-worklets");
1770
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1771
+ function applyDetent(value, centered, width = 0.05) {
1772
+ if (centered && Math.abs(value) < width) return 0;
1773
+ return value;
1774
+ }
1775
+ function Slider({
1776
+ value,
1777
+ min = -1,
1778
+ max = 1,
1779
+ centered = true,
1780
+ onStart,
1781
+ onChange,
1782
+ onEnd,
1783
+ testID,
1784
+ valueLabel,
1785
+ disabled = false
1786
+ }) {
1787
+ const theme = useTheme();
1788
+ const [width, setWidth] = (0, import_react12.useState)(0);
1789
+ const widthRef = (0, import_react12.useRef)(0);
1790
+ const startValue = (0, import_react12.useRef)(value);
1791
+ const valueRef = (0, import_react12.useRef)(value);
1792
+ valueRef.current = value;
1793
+ const onLayout = (e) => {
1794
+ setWidth(e.nativeEvent.layout.width);
1795
+ widthRef.current = e.nativeEvent.layout.width;
1796
+ };
1797
+ const handlers = (0, import_react12.useMemo)(() => {
1798
+ const begin = () => {
1799
+ startValue.current = valueRef.current;
1800
+ onStart?.();
1801
+ };
1802
+ const update = (tx) => {
1803
+ const w = widthRef.current;
1804
+ if (w === 0) return;
1805
+ const range = max - min;
1806
+ const raw = startValue.current + tx / w * range;
1807
+ const clamped = Math.min(max, Math.max(min, raw));
1808
+ onChange(applyDetent(clamped, centered && min < 0));
1809
+ };
1810
+ const finish = () => onEnd?.();
1811
+ return { begin, update, finish };
1812
+ }, [min, max, centered, onChange, onStart, onEnd]);
1813
+ const gesture = (0, import_react12.useMemo)(
1814
+ () => import_react_native_gesture_handler6.Gesture.Pan().enabled(!disabled).onStart(() => {
1815
+ "worklet";
1816
+ (0, import_react_native_worklets6.runOnJS)(handlers.begin)();
1817
+ }).onUpdate((e) => {
1818
+ "worklet";
1819
+ (0, import_react_native_worklets6.runOnJS)(handlers.update)(e.translationX);
1820
+ }).onFinalize(() => {
1821
+ "worklet";
1822
+ (0, import_react_native_worklets6.runOnJS)(handlers.finish)();
1823
+ }),
1824
+ [handlers, disabled]
1825
+ );
1826
+ const t = (value - min) / (max - min);
1827
+ const thumbX = width * t;
1828
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native_gesture_handler6.GestureDetector, { gesture, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1829
+ import_react_native_reanimated6.default.View,
1830
+ {
1831
+ testID,
1832
+ onLayout,
1833
+ style: {
1834
+ height: valueLabel ? 58 : 36,
1835
+ justifyContent: "flex-end",
1836
+ paddingBottom: 9,
1837
+ opacity: disabled ? 0.4 : 1
1838
+ },
1839
+ children: [
1840
+ valueLabel ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1841
+ import_react_native8.View,
1842
+ {
1843
+ pointerEvents: "none",
1844
+ style: {
1845
+ position: "absolute",
1846
+ top: 0,
1847
+ left: Math.max(0, Math.min(width - 44, thumbX - 22)),
1848
+ width: 44,
1849
+ alignItems: "center",
1850
+ paddingVertical: 3,
1851
+ borderRadius: 10,
1852
+ backgroundColor: theme.colors.surface
1853
+ },
1854
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native8.Text, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm }, children: valueLabel })
1855
+ }
1856
+ ) : null,
1857
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native8.View, { style: { height: 2, borderRadius: 1, backgroundColor: theme.colors.track } }),
1858
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1859
+ import_react_native8.View,
1860
+ {
1861
+ style: {
1862
+ position: "absolute",
1863
+ bottom: 9,
1864
+ left: centered && min < 0 ? Math.min(width / 2, thumbX) : 0,
1865
+ width: centered && min < 0 ? Math.abs(thumbX - width / 2) : thumbX,
1866
+ height: 2,
1867
+ borderRadius: 1,
1868
+ backgroundColor: theme.colors.accent
1869
+ }
1870
+ }
1871
+ ),
1872
+ centered && min < 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1873
+ import_react_native8.View,
1874
+ {
1875
+ style: {
1876
+ position: "absolute",
1877
+ left: width / 2 - 1,
1878
+ width: 2,
1879
+ height: 10,
1880
+ backgroundColor: theme.colors.textMuted
1881
+ }
1882
+ }
1883
+ ) : null,
1884
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1885
+ import_react_native8.View,
1886
+ {
1887
+ style: {
1888
+ position: "absolute",
1889
+ bottom: 1,
1890
+ left: Math.max(0, Math.min(width - 18, thumbX - 9)),
1891
+ width: 18,
1892
+ height: 18,
1893
+ borderRadius: 9,
1894
+ backgroundColor: theme.colors.thumb
1895
+ }
1896
+ }
1897
+ )
1898
+ ]
1899
+ }
1900
+ ) });
1901
+ }
1902
+
1903
+ // src/panels/AdjustPanel.tsx
1904
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1905
+ var LABELS = {
1906
+ brightness: "Brightness",
1907
+ contrast: "Contrast",
1908
+ saturation: "Saturation",
1909
+ warmth: "Warmth",
1910
+ tint: "Tint",
1911
+ exposure: "Exposure",
1912
+ highlights: "Highlights",
1913
+ shadows: "Shadows",
1914
+ fade: "Fade",
1915
+ structure: "Structure",
1916
+ sharpen: "Sharpen",
1917
+ grain: "Grain",
1918
+ vignette: "Vignette"
1919
+ };
1920
+ function AdjustPanel() {
1921
+ const { dispatch } = (0, import_core12.useEditor)();
1922
+ const store = (0, import_core12.useEditorStore)();
1923
+ const theme = useTheme();
1924
+ const layer = useTargetImageLayer();
1925
+ const keys = store.registry.adjustments;
1926
+ const [activeKey, setActiveKey] = (0, import_react13.useState)(keys[0] ?? "brightness");
1927
+ if (!layer) return null;
1928
+ const adjustments = layer.data.color.adjustments ?? {};
1929
+ const value = adjustments[activeKey] ?? 0;
1930
+ const touchedActive = (0, import_core12.hasAdjustment)(adjustments, activeKey);
1931
+ const resetActive = () => dispatch({
1932
+ type: "layer/patchData",
1933
+ id: layer.id,
1934
+ patch: (0, import_core12.adjustmentResetPatch)(activeKey)
1935
+ });
1936
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native9.View, { style: { backgroundColor: theme.colors.background }, children: [
1937
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native9.View, { style: { paddingHorizontal: theme.spacing.lg }, children: [
1938
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1939
+ import_react_native9.View,
1940
+ {
1941
+ style: {
1942
+ flexDirection: "row",
1943
+ alignItems: "center",
1944
+ justifyContent: "center",
1945
+ gap: theme.spacing.sm,
1946
+ minHeight: 28
1947
+ },
1948
+ children: [
1949
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1950
+ import_react_native9.Text,
1951
+ {
1952
+ style: {
1953
+ color: theme.colors.textMuted,
1954
+ textAlign: "center",
1955
+ fontSize: theme.font.sizeSm
1956
+ },
1957
+ children: [
1958
+ LABELS[activeKey] ?? activeKey,
1959
+ " ",
1960
+ value === 0 ? "" : (value > 0 ? "+" : "") + Math.round(value * 100)
1961
+ ]
1962
+ }
1963
+ ),
1964
+ touchedActive ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1965
+ import_react_native9.Pressable,
1966
+ {
1967
+ accessibilityRole: "button",
1968
+ accessibilityLabel: `Reset ${LABELS[activeKey] ?? activeKey}`,
1969
+ onPress: resetActive,
1970
+ hitSlop: 8,
1971
+ style: {
1972
+ paddingHorizontal: theme.spacing.sm,
1973
+ paddingVertical: 2,
1974
+ borderRadius: theme.radii.sm,
1975
+ borderWidth: 1,
1976
+ borderColor: theme.colors.track
1977
+ },
1978
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native9.Text, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm }, children: "Reset" })
1979
+ }
1980
+ ) : null
1981
+ ]
1982
+ }
1983
+ ),
1984
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1985
+ Slider,
1986
+ {
1987
+ value,
1988
+ min: -1,
1989
+ max: 1,
1990
+ centered: true,
1991
+ valueLabel: `${value > 0 ? "+" : ""}${Math.round(value * 100)}`,
1992
+ onStart: () => dispatch({ type: "history/beginBatch", label: activeKey }),
1993
+ onChange: (v) => dispatch({
1994
+ type: "layer/patchData",
1995
+ id: layer.id,
1996
+ patch: { color: { adjustments: { [activeKey]: v } } }
1997
+ }),
1998
+ onEnd: () => dispatch({ type: "history/endBatch" })
1999
+ }
2000
+ )
2001
+ ] }),
2002
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2003
+ import_react_native9.ScrollView,
2004
+ {
2005
+ horizontal: true,
2006
+ showsHorizontalScrollIndicator: false,
2007
+ contentContainerStyle: { gap: theme.spacing.sm, padding: theme.spacing.md },
2008
+ style: { flexGrow: 0 },
2009
+ children: keys.map((key) => {
2010
+ const touched = (adjustments[key] ?? 0) !== 0;
2011
+ const active = key === activeKey;
2012
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2013
+ import_react_native9.Pressable,
2014
+ {
2015
+ onPress: () => setActiveKey(key),
2016
+ style: { alignItems: "center", gap: 6, width: 76 },
2017
+ children: [
2018
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native9.View, { style: { width: 48, height: 48, borderRadius: 24, borderWidth: 1, borderColor: active ? theme.colors.accent : theme.colors.textMuted, backgroundColor: active ? theme.colors.surface : "transparent", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native9.Text, { style: { color: active ? theme.colors.accent : theme.colors.text, fontSize: 18 }, children: (LABELS[key] ?? key).slice(0, 1) }) }),
2019
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2020
+ import_react_native9.Text,
2021
+ {
2022
+ style: {
2023
+ color: active ? theme.colors.accent : touched ? theme.colors.text : theme.colors.textMuted,
2024
+ fontSize: theme.font.sizeSm,
2025
+ fontWeight: theme.font.weight
2026
+ },
2027
+ children: [
2028
+ LABELS[key] ?? key,
2029
+ touched ? " \u2022" : ""
2030
+ ]
2031
+ }
2032
+ )
2033
+ ]
2034
+ },
2035
+ key
2036
+ );
2037
+ })
2038
+ }
2039
+ )
2040
+ ] });
2041
+ }
2042
+
2043
+ // src/panels/AddPhotoPanel.tsx
2044
+ var import_core14 = require("@pictor-sdk/core");
2045
+ var import_react14 = require("react");
2046
+ var import_react_native10 = require("react-native");
2047
+
2048
+ // src/panels/addPhotoController.ts
2049
+ var import_core13 = require("@pictor-sdk/core");
2050
+ function isInsertable(value) {
2051
+ if (typeof value !== "object" || value === null) return false;
2052
+ const source = value.source;
2053
+ return source !== void 0 && source !== null && source !== "";
2054
+ }
2055
+ function toDenseImages(value) {
2056
+ if (!Array.isArray(value)) return null;
2057
+ const dense = [];
2058
+ for (let index = 0; index < value.length; index++) {
2059
+ if (!(index in value)) return null;
2060
+ const entry = value[index];
2061
+ if (!isInsertable(entry)) return null;
2062
+ dense.push(entry);
2063
+ }
2064
+ return dense;
2065
+ }
2066
+ function isDispatchable(command) {
2067
+ return typeof command === "object" && command !== null && typeof command.type === "string";
2068
+ }
2069
+ var InvalidImageResultError = class extends Error {
2070
+ constructor() {
2071
+ super("requestImages resolved with something that is not a list of images");
2072
+ this.name = "InvalidImageResultError";
2073
+ }
2074
+ };
2075
+ function createAddPhotoController({
2076
+ requestImages,
2077
+ dispatch,
2078
+ select,
2079
+ onError,
2080
+ insert
2081
+ }) {
2082
+ let pending = false;
2083
+ let disposed = false;
2084
+ return {
2085
+ isPending: () => pending,
2086
+ isAvailable: () => typeof requestImages === "function",
2087
+ dispose: () => {
2088
+ disposed = true;
2089
+ },
2090
+ async request() {
2091
+ if (!requestImages || pending || disposed) return;
2092
+ pending = true;
2093
+ try {
2094
+ const result = await requestImages();
2095
+ if (disposed) return;
2096
+ if (result === null || result === void 0) return;
2097
+ const images = toDenseImages(result);
2098
+ if (!images) {
2099
+ onError?.(new InvalidImageResultError());
2100
+ return;
2101
+ }
2102
+ if (images.length === 0) return;
2103
+ const layers = (0, import_core13.createImageLayers)(images, insert);
2104
+ const commands = (0, import_core13.insertImageCommands)(layers, insert);
2105
+ if (!commands.every(isDispatchable)) {
2106
+ onError?.(new InvalidImageResultError());
2107
+ return;
2108
+ }
2109
+ for (const command of commands) dispatch(command);
2110
+ const last = layers[layers.length - 1];
2111
+ if (last) select([last.id]);
2112
+ } catch (error) {
2113
+ if (!disposed) onError?.(error);
2114
+ } finally {
2115
+ pending = false;
2116
+ }
2117
+ }
2118
+ };
2119
+ }
2120
+
2121
+ // src/panels/AddPhotoPanel.tsx
2122
+ var import_jsx_runtime13 = require("react/jsx-runtime");
2123
+ function AddPhotoPanel() {
2124
+ const { dispatch, select } = (0, import_core14.useEditor)();
2125
+ const host = useHostCallbacks();
2126
+ const theme = useTheme();
2127
+ const [pending, setPending] = (0, import_react14.useState)(false);
2128
+ const controller = (0, import_react14.useMemo)(
2129
+ () => createAddPhotoController({
2130
+ // Read through the ref at call time, so a host re-creating its closure
2131
+ // does not rebuild the controller mid-request.
2132
+ requestImages: host.current.requestImages ? () => host.current.requestImages() : void 0,
2133
+ dispatch,
2134
+ select,
2135
+ onError: (error) => host.current.onError?.(error)
2136
+ }),
2137
+ [dispatch, select, host]
2138
+ );
2139
+ (0, import_react14.useEffect)(() => () => controller.dispose(), [controller]);
2140
+ const available = controller.isAvailable();
2141
+ const press = async () => {
2142
+ if (!available || pending) return;
2143
+ setPending(true);
2144
+ try {
2145
+ await controller.request();
2146
+ } finally {
2147
+ setPending(false);
2148
+ }
2149
+ };
2150
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_react_native10.View, { style: { backgroundColor: theme.colors.background, padding: theme.spacing.lg }, children: [
2151
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2152
+ import_react_native10.Pressable,
2153
+ {
2154
+ accessibilityRole: "button",
2155
+ accessibilityLabel: "Add photos",
2156
+ accessibilityState: { disabled: !available || pending },
2157
+ disabled: !available || pending,
2158
+ onPress: press,
2159
+ style: {
2160
+ paddingVertical: theme.spacing.md,
2161
+ borderRadius: theme.radii.sm,
2162
+ borderWidth: 1,
2163
+ borderColor: available ? theme.colors.accent : theme.colors.track,
2164
+ alignItems: "center",
2165
+ opacity: available && !pending ? 1 : 0.45
2166
+ },
2167
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2168
+ import_react_native10.Text,
2169
+ {
2170
+ style: {
2171
+ color: available ? theme.colors.accent : theme.colors.textMuted,
2172
+ fontSize: theme.font.size,
2173
+ fontWeight: theme.font.weight
2174
+ },
2175
+ children: pending ? "Choosing\u2026" : "Add photos"
2176
+ }
2177
+ )
2178
+ }
2179
+ ),
2180
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2181
+ import_react_native10.Text,
2182
+ {
2183
+ style: {
2184
+ color: theme.colors.textMuted,
2185
+ fontSize: theme.font.sizeSm,
2186
+ textAlign: "center",
2187
+ marginTop: theme.spacing.sm
2188
+ },
2189
+ children: available ? "Pick one or more photos to place on the story" : `Unavailable \u2014 pass \`requestImages\` to <Editor>`
2190
+ }
2191
+ )
2192
+ ] });
2193
+ }
2194
+
2195
+ // src/panels/ColorPanel.tsx
2196
+ var import_core15 = require("@pictor-sdk/core");
2197
+ var import_react15 = require("react");
2198
+ var import_react_native11 = require("react-native");
2199
+ var import_jsx_runtime14 = require("react/jsx-runtime");
2200
+ var TINT_SWATCHES = [
2201
+ "#1f8a8a",
2202
+ "#2f6fd0",
2203
+ "#7a5cd0",
2204
+ "#c04a9e",
2205
+ "#c8443c",
2206
+ "#d99038",
2207
+ "#8a9a3f",
2208
+ "#3f9a6a"
2209
+ ];
2210
+ var ENDS = [
2211
+ { key: "shadows", label: "Shadows" },
2212
+ { key: "highlights", label: "Highlights" }
2213
+ ];
2214
+ var INITIAL_STRENGTH = 0.5;
2215
+ function ColorPanel() {
2216
+ const { dispatch } = (0, import_core15.useEditor)();
2217
+ const theme = useTheme();
2218
+ const layer = useTargetImageLayer();
2219
+ const [end, setEnd] = (0, import_react15.useState)("shadows");
2220
+ if (!layer) return null;
2221
+ const data = layer.data;
2222
+ const tint = data.color.tinting?.[end];
2223
+ const active = (0, import_core15.isInertTint)(tint) ? void 0 : tint;
2224
+ const patch = (next) => dispatch({
2225
+ type: "layer/patchData",
2226
+ id: layer.id,
2227
+ patch: { color: (0, import_core15.tintPatch)(end, next) }
2228
+ });
2229
+ const pick = (color) => patch(
2230
+ active?.color === color ? void 0 : { color, strength: active?.strength ?? INITIAL_STRENGTH }
2231
+ );
2232
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native11.View, { style: { backgroundColor: theme.colors.background, paddingBottom: theme.spacing.md }, children: [
2233
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2234
+ import_react_native11.View,
2235
+ {
2236
+ style: {
2237
+ flexDirection: "row",
2238
+ justifyContent: "center",
2239
+ gap: theme.spacing.md,
2240
+ paddingTop: theme.spacing.sm
2241
+ },
2242
+ children: ENDS.map(({ key, label }) => {
2243
+ const selected = key === end;
2244
+ const set = !(0, import_core15.isInertTint)(data.color.tinting?.[key]);
2245
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2246
+ import_react_native11.Pressable,
2247
+ {
2248
+ accessibilityRole: "button",
2249
+ accessibilityLabel: `Tint ${label.toLowerCase()}`,
2250
+ onPress: () => setEnd(key),
2251
+ style: {
2252
+ paddingHorizontal: theme.spacing.md,
2253
+ paddingVertical: 6,
2254
+ borderRadius: theme.radii.sm,
2255
+ borderWidth: 1,
2256
+ borderColor: selected ? theme.colors.accent : "transparent"
2257
+ },
2258
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
2259
+ import_react_native11.Text,
2260
+ {
2261
+ style: {
2262
+ color: selected ? theme.colors.accent : theme.colors.textMuted,
2263
+ fontSize: theme.font.sizeSm,
2264
+ fontWeight: theme.font.weight
2265
+ },
2266
+ children: [
2267
+ label,
2268
+ set ? " \u2022" : ""
2269
+ ]
2270
+ }
2271
+ )
2272
+ },
2273
+ key
2274
+ );
2275
+ })
2276
+ }
2277
+ ),
2278
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2279
+ import_react_native11.ScrollView,
2280
+ {
2281
+ horizontal: true,
2282
+ showsHorizontalScrollIndicator: false,
2283
+ contentContainerStyle: { gap: theme.spacing.sm, paddingHorizontal: theme.spacing.lg, paddingVertical: theme.spacing.md },
2284
+ style: { flexGrow: 0 },
2285
+ children: TINT_SWATCHES.map((color) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2286
+ import_react_native11.Pressable,
2287
+ {
2288
+ accessibilityRole: "button",
2289
+ accessibilityLabel: `Tint ${end} ${color}`,
2290
+ onPress: () => pick(color),
2291
+ style: {
2292
+ width: 40,
2293
+ height: 40,
2294
+ borderRadius: 20,
2295
+ backgroundColor: color,
2296
+ borderWidth: active?.color === color ? 3 : 1,
2297
+ borderColor: active?.color === color ? theme.colors.text : theme.colors.track
2298
+ }
2299
+ },
2300
+ color
2301
+ ))
2302
+ }
2303
+ ),
2304
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native11.View, { style: { paddingHorizontal: theme.spacing.lg }, children: [
2305
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
2306
+ import_react_native11.View,
2307
+ {
2308
+ style: {
2309
+ flexDirection: "row",
2310
+ alignItems: "center",
2311
+ justifyContent: "center",
2312
+ gap: theme.spacing.sm,
2313
+ minHeight: 28
2314
+ },
2315
+ children: [
2316
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native11.Text, { style: { color: theme.colors.textMuted, fontSize: theme.font.sizeSm }, children: active ? `Strength ${Math.round(active.strength * 100)}` : "Pick a colour" }),
2317
+ active ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2318
+ import_react_native11.Pressable,
2319
+ {
2320
+ accessibilityRole: "button",
2321
+ accessibilityLabel: `Reset ${end} tint`,
2322
+ onPress: () => patch(void 0),
2323
+ hitSlop: 8,
2324
+ style: {
2325
+ paddingHorizontal: theme.spacing.sm,
2326
+ paddingVertical: 2,
2327
+ borderRadius: theme.radii.sm,
2328
+ borderWidth: 1,
2329
+ borderColor: theme.colors.track
2330
+ },
2331
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native11.Text, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm }, children: "Reset" })
2332
+ }
2333
+ ) : null
2334
+ ]
2335
+ }
2336
+ ),
2337
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2338
+ Slider,
2339
+ {
2340
+ value: active?.strength ?? 0,
2341
+ min: 0,
2342
+ max: 1,
2343
+ centered: false,
2344
+ testID: "tint-strength",
2345
+ disabled: !active,
2346
+ valueLabel: `${Math.round((active?.strength ?? 0) * 100)}`,
2347
+ onStart: () => dispatch({ type: "history/beginBatch", label: `tint-${end}` }),
2348
+ onChange: (strength) => {
2349
+ if (!active) return;
2350
+ patch({ color: active.color, strength });
2351
+ },
2352
+ onEnd: () => dispatch({ type: "history/endBatch" })
2353
+ }
2354
+ )
2355
+ ] })
2356
+ ] });
2357
+ }
2358
+
2359
+ // src/panels/CropPanel.tsx
2360
+ var import_core16 = require("@pictor-sdk/core");
2361
+ var import_react_native13 = require("react-native");
2362
+
2363
+ // src/components/StraightenRuler.tsx
2364
+ var import_react16 = require("react");
2365
+ var import_react_native12 = require("react-native");
2366
+ var import_react_native_gesture_handler7 = require("react-native-gesture-handler");
2367
+ var import_react_native_reanimated7 = __toESM(require("react-native-reanimated"));
2368
+ var import_react_native_worklets7 = require("react-native-worklets");
2369
+ var import_jsx_runtime15 = require("react/jsx-runtime");
2370
+ var PX_PER_DEGREE = 60;
2371
+ function StraightenRuler({ value, min = -45, max = 45, onStart, onChange, onEnd }) {
2372
+ const theme = useTheme();
2373
+ const start = (0, import_react16.useRef)(value);
2374
+ const current = (0, import_react16.useRef)(value);
2375
+ current.current = value;
2376
+ const handlers = (0, import_react16.useMemo)(() => ({
2377
+ begin: () => {
2378
+ start.current = current.current;
2379
+ onStart?.();
2380
+ },
2381
+ update: (translationX) => {
2382
+ const raw = Math.min(max, Math.max(min, start.current - translationX / PX_PER_DEGREE));
2383
+ onChange(Math.abs(raw) < 0.15 ? 0 : raw);
2384
+ },
2385
+ finish: () => onEnd?.()
2386
+ }), [min, max, onStart, onChange, onEnd]);
2387
+ const gesture = (0, import_react16.useMemo)(() => import_react_native_gesture_handler7.Gesture.Pan().onStart(() => {
2388
+ "worklet";
2389
+ (0, import_react_native_worklets7.runOnJS)(handlers.begin)();
2390
+ }).onUpdate((event) => {
2391
+ "worklet";
2392
+ (0, import_react_native_worklets7.runOnJS)(handlers.update)(event.translationX);
2393
+ }).onFinalize(() => {
2394
+ "worklet";
2395
+ (0, import_react_native_worklets7.runOnJS)(handlers.finish)();
2396
+ }), [handlers]);
2397
+ const rounded = Math.round(value * 10) / 10;
2398
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native_gesture_handler7.GestureDetector, { gesture, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_react_native_reanimated7.default.View, { style: { height: 76, overflow: "hidden", justifyContent: "flex-end" }, children: [
2399
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native12.View, { style: { position: "absolute", top: 0, alignSelf: "center", paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12, backgroundColor: theme.colors.surface }, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_react_native12.Text, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm }, children: [
2400
+ rounded.toFixed(1),
2401
+ "\xB0"
2402
+ ] }) }),
2403
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native12.View, { style: { height: 34, flexDirection: "row", justifyContent: "center", alignItems: "flex-end", gap: 8 }, children: Array.from({ length: 31 }, (_, index) => {
2404
+ const offset = index - 15;
2405
+ const major = offset % 5 === 0;
2406
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native12.View, { style: { width: 1, height: major ? 22 : 11, backgroundColor: offset === 0 ? theme.colors.accent : theme.colors.textMuted } }, offset);
2407
+ }) }),
2408
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native12.View, { style: { position: "absolute", bottom: 0, left: "50%", width: 2, height: 30, backgroundColor: theme.colors.accent } })
2409
+ ] }) });
2410
+ }
2411
+
2412
+ // src/panels/cropMath.ts
2413
+ function canvasSizeForAspect(current, aspect) {
2414
+ if (aspect === "free") return current;
2415
+ return { width: current.width, height: Math.round(current.width / aspect) };
2416
+ }
2417
+ function centeredCropForAspect(intrinsic, aspect) {
2418
+ const sourceAspect = intrinsic.width / intrinsic.height;
2419
+ if (sourceAspect > aspect) {
2420
+ const width = aspect / sourceAspect;
2421
+ return { x: (1 - width) / 2, y: 0, width, height: 1 };
2422
+ }
2423
+ const height = sourceAspect / aspect;
2424
+ return { x: 0, y: (1 - height) / 2, width: 1, height };
2425
+ }
2426
+ var STRAIGHTEN_LIMIT = 45 * Math.PI / 180;
2427
+ function straightenToRadians(value) {
2428
+ return Math.max(-1, Math.min(1, value)) * STRAIGHTEN_LIMIT;
2429
+ }
2430
+ function radiansToStraighten(rad) {
2431
+ return Math.max(-1, Math.min(1, rad / STRAIGHTEN_LIMIT));
2432
+ }
2433
+
2434
+ // src/panels/CropPanel.tsx
2435
+ var import_jsx_runtime16 = require("react/jsx-runtime");
2436
+ function aspectLabel(aspect) {
2437
+ if (aspect === "free") return "Free";
2438
+ const known = [
2439
+ [4 / 5, "4:5"],
2440
+ [1, "1:1"],
2441
+ [16 / 9, "16:9"],
2442
+ [9 / 16, "9:16"],
2443
+ [3 / 4, "3:4"],
2444
+ [4 / 3, "4:3"]
2445
+ ];
2446
+ for (const [value, label] of known) {
2447
+ if (Math.abs(aspect - value) < 1e-6) return label;
2448
+ }
2449
+ return aspect.toFixed(2);
2450
+ }
2451
+ function CropPanel() {
2452
+ const { document, dispatch, profile } = (0, import_core16.useEditor)();
2453
+ (0, import_core16.useEditorStore)();
2454
+ const theme = useTheme();
2455
+ const layer = useTargetImageLayer();
2456
+ if (!layer) return null;
2457
+ const data = layer.data;
2458
+ const straighten = data.straighten ?? 0;
2459
+ const applyAspect = (aspect) => {
2460
+ const nextSize = canvasSizeForAspect(document.size, aspect);
2461
+ dispatch({ type: "history/beginBatch", label: "aspect" });
2462
+ dispatch({ type: "doc/patch", patch: { size: nextSize } });
2463
+ dispatch({ type: "layer/patchData", id: layer.id, patch: { crop: void 0 } });
2464
+ dispatch({
2465
+ type: "layer/transform",
2466
+ id: layer.id,
2467
+ patch: {
2468
+ x: 0.5,
2469
+ y: 0.5,
2470
+ scale: (0, import_core16.cropFitScale)(data.intrinsic, nextSize, profile.interaction?.crop?.fit ?? "contain")
2471
+ }
2472
+ });
2473
+ dispatch({ type: "history/endBatch" });
2474
+ };
2475
+ const rotate90 = () => {
2476
+ const current = layer.transform.rotation;
2477
+ const rotation = current + Math.PI / 2;
2478
+ dispatch({
2479
+ type: "layer/transform",
2480
+ id: layer.id,
2481
+ patch: {
2482
+ rotation,
2483
+ x: 0.5,
2484
+ y: 0.5,
2485
+ scale: (0, import_core16.cropFitScale)(data.intrinsic, document.size, profile.interaction?.crop?.fit ?? "contain", rotation)
2486
+ }
2487
+ });
2488
+ };
2489
+ const flip = (axis) => dispatch({
2490
+ type: "layer/transform",
2491
+ id: layer.id,
2492
+ patch: (0, import_core16.flipPatch)(layer.transform, axis)
2493
+ });
2494
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native13.View, { style: { backgroundColor: theme.colors.background }, children: [
2495
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native13.View, { style: { paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2496
+ StraightenRuler,
2497
+ {
2498
+ value: straighten * 180 / Math.PI,
2499
+ min: -45,
2500
+ max: 45,
2501
+ onStart: () => dispatch({ type: "history/beginBatch", label: "straighten" }),
2502
+ onChange: (v) => dispatch({
2503
+ type: "layer/patchData",
2504
+ id: layer.id,
2505
+ patch: { straighten: v * Math.PI / 180 }
2506
+ }),
2507
+ onEnd: () => dispatch({ type: "history/endBatch" })
2508
+ }
2509
+ ) }),
2510
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
2511
+ import_react_native13.ScrollView,
2512
+ {
2513
+ horizontal: true,
2514
+ showsHorizontalScrollIndicator: false,
2515
+ contentContainerStyle: { gap: theme.spacing.sm, padding: theme.spacing.md },
2516
+ style: { flexGrow: 0 },
2517
+ children: [
2518
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2519
+ import_react_native13.Pressable,
2520
+ {
2521
+ accessibilityRole: "button",
2522
+ accessibilityLabel: "Rotate 90 degrees",
2523
+ onPress: rotate90,
2524
+ style: {
2525
+ paddingHorizontal: theme.spacing.md,
2526
+ paddingVertical: theme.spacing.sm,
2527
+ borderRadius: theme.radii.md,
2528
+ backgroundColor: theme.colors.surface
2529
+ },
2530
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native13.Text, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm, fontWeight: theme.font.weight }, children: "\u27F3 90\xB0" })
2531
+ }
2532
+ ),
2533
+ ["horizontal", "vertical"].map((axis) => {
2534
+ const on = (0, import_core16.isFlipped)(layer.transform, axis);
2535
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2536
+ import_react_native13.Pressable,
2537
+ {
2538
+ accessibilityRole: "button",
2539
+ accessibilityLabel: `Flip ${axis}`,
2540
+ accessibilityState: { selected: on },
2541
+ onPress: () => flip(axis),
2542
+ style: {
2543
+ paddingHorizontal: theme.spacing.md,
2544
+ paddingVertical: theme.spacing.sm,
2545
+ borderRadius: theme.radii.md,
2546
+ backgroundColor: theme.colors.surface,
2547
+ borderWidth: 1,
2548
+ borderColor: on ? theme.colors.accent : "transparent"
2549
+ },
2550
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2551
+ import_react_native13.Text,
2552
+ {
2553
+ style: {
2554
+ color: on ? theme.colors.accent : theme.colors.text,
2555
+ fontSize: theme.font.sizeSm,
2556
+ fontWeight: theme.font.weight
2557
+ },
2558
+ children: axis === "horizontal" ? "\u21CB" : "\u21C5"
2559
+ }
2560
+ )
2561
+ },
2562
+ axis
2563
+ );
2564
+ }),
2565
+ profile.aspects.map((aspect, i) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2566
+ import_react_native13.Pressable,
2567
+ {
2568
+ onPress: () => applyAspect(aspect),
2569
+ style: {
2570
+ width: 58,
2571
+ height: 48,
2572
+ borderRadius: theme.radii.md,
2573
+ borderWidth: 1,
2574
+ borderColor: theme.colors.textMuted,
2575
+ backgroundColor: "transparent",
2576
+ alignItems: "center",
2577
+ justifyContent: "center"
2578
+ },
2579
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native13.Text, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm, fontWeight: theme.font.weight }, children: aspectLabel(aspect) })
2580
+ },
2581
+ i
2582
+ ))
2583
+ ]
2584
+ }
2585
+ )
2586
+ ] });
2587
+ }
2588
+
2589
+ // src/panels/DrawPanel.tsx
2590
+ var import_react_native14 = require("react-native");
2591
+ var import_jsx_runtime17 = require("react/jsx-runtime");
2592
+ var BRUSHES = [
2593
+ { id: "pen", label: "Pen" },
2594
+ { id: "marker", label: "Marker" },
2595
+ { id: "neon", label: "Neon" },
2596
+ { id: "highlighter", label: "Highlight" },
2597
+ { id: "eraser", label: "Eraser" }
2598
+ ];
2599
+ var SWATCHES = [
2600
+ "#ffffff",
2601
+ "#000000",
2602
+ "#ed4956",
2603
+ "#ffb43b",
2604
+ "#ffe25c",
2605
+ "#78de85",
2606
+ "#4dc4ff",
2607
+ "#3897f0",
2608
+ "#a15fed",
2609
+ "#d977b1"
2610
+ ];
2611
+ function DrawPanel() {
2612
+ const theme = useTheme();
2613
+ const [brush, setBrush] = useUiState("draw.brush", DEFAULT_BRUSH);
2614
+ const [eyedropper, setEyedropper] = useUiState("draw.eyedropper", false);
2615
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_react_native14.View, { style: { backgroundColor: theme.colors.background }, children: [
2616
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native14.View, { style: { paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2617
+ Slider,
2618
+ {
2619
+ value: brush.width,
2620
+ min: 2e-3,
2621
+ max: 0.06,
2622
+ centered: false,
2623
+ onChange: (width) => setBrush({ ...brush, width })
2624
+ }
2625
+ ) }),
2626
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
2627
+ import_react_native14.ScrollView,
2628
+ {
2629
+ horizontal: true,
2630
+ showsHorizontalScrollIndicator: false,
2631
+ contentContainerStyle: { gap: theme.spacing.sm, paddingHorizontal: theme.spacing.md, alignItems: "center" },
2632
+ style: { flexGrow: 0 },
2633
+ children: [
2634
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2635
+ import_react_native14.Pressable,
2636
+ {
2637
+ onPress: () => setEyedropper(!eyedropper),
2638
+ style: {
2639
+ width: 28,
2640
+ height: 28,
2641
+ borderRadius: 14,
2642
+ borderWidth: 2,
2643
+ borderColor: eyedropper ? theme.colors.accent : theme.colors.textMuted,
2644
+ alignItems: "center",
2645
+ justifyContent: "center"
2646
+ },
2647
+ children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native14.Text, { style: { color: theme.colors.text, fontSize: 13 }, children: "\u{1F4A7}" })
2648
+ }
2649
+ ),
2650
+ SWATCHES.map((color) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2651
+ import_react_native14.Pressable,
2652
+ {
2653
+ onPress: () => setBrush({ ...brush, color }),
2654
+ style: {
2655
+ width: 26,
2656
+ height: 26,
2657
+ borderRadius: 13,
2658
+ backgroundColor: color,
2659
+ borderWidth: brush.color === color ? 3 : 1,
2660
+ borderColor: brush.color === color ? theme.colors.accent : theme.colors.track
2661
+ }
2662
+ },
2663
+ color
2664
+ ))
2665
+ ]
2666
+ }
2667
+ ),
2668
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2669
+ import_react_native14.ScrollView,
2670
+ {
2671
+ horizontal: true,
2672
+ showsHorizontalScrollIndicator: false,
2673
+ contentContainerStyle: { gap: theme.spacing.sm, padding: theme.spacing.md },
2674
+ style: { flexGrow: 0 },
2675
+ children: BRUSHES.map((b) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2676
+ import_react_native14.Pressable,
2677
+ {
2678
+ onPress: () => setBrush({ ...brush, brush: b.id }),
2679
+ style: {
2680
+ paddingHorizontal: theme.spacing.md,
2681
+ paddingVertical: theme.spacing.sm,
2682
+ borderRadius: theme.radii.md,
2683
+ backgroundColor: brush.brush === b.id ? theme.colors.surface : "transparent"
2684
+ },
2685
+ children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2686
+ import_react_native14.Text,
2687
+ {
2688
+ style: {
2689
+ color: brush.brush === b.id ? theme.colors.accent : theme.colors.text,
2690
+ fontSize: theme.font.sizeSm,
2691
+ fontWeight: theme.font.weight
2692
+ },
2693
+ children: b.label
2694
+ }
2695
+ )
2696
+ },
2697
+ b.id
2698
+ ))
2699
+ }
2700
+ )
2701
+ ] });
2702
+ }
2703
+
2704
+ // src/panels/FilterStrip.tsx
2705
+ var import_core17 = require("@pictor-sdk/core");
2706
+ var import_skia7 = require("@pictor-sdk/skia");
2707
+ var import_react_native_skia = require("@shopify/react-native-skia");
2708
+ var import_react17 = require("react");
2709
+ var import_react_native15 = require("react-native");
2710
+ var import_jsx_runtime18 = require("react/jsx-runtime");
2711
+ var THUMB = 84;
2712
+ function useBaseImageLayer() {
2713
+ const { document } = (0, import_core17.useEditor)();
2714
+ return (0, import_core17.baseImageLayer)(document);
2715
+ }
2716
+ function Thumb({ lut, label, active }) {
2717
+ const { document } = (0, import_core17.useEditor)();
2718
+ const renderer = (0, import_skia7.useRenderer)();
2719
+ const theme = useTheme();
2720
+ const [image, setImage] = (0, import_react17.useState)(null);
2721
+ const lutId = lut?.id;
2722
+ const lutIntensity = lut?.intensity;
2723
+ (0, import_react17.useEffect)(() => {
2724
+ let alive = true;
2725
+ const requestedLut = lutId === void 0 ? void 0 : { id: lutId, intensity: lutIntensity ?? 1 };
2726
+ renderer.renderThumbnailImage(document, requestedLut, THUMB).then((img) => alive && setImage(img)).catch(() => {
2727
+ });
2728
+ return () => {
2729
+ alive = false;
2730
+ };
2731
+ }, [renderer, document, lutId, lutIntensity]);
2732
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native15.View, { style: { alignItems: "center", gap: 4 }, children: [
2733
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2734
+ import_react_native15.Text,
2735
+ {
2736
+ style: {
2737
+ color: active ? theme.colors.accent : theme.colors.textMuted,
2738
+ fontSize: theme.font.sizeSm
2739
+ },
2740
+ children: label
2741
+ }
2742
+ ),
2743
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2744
+ import_react_native15.View,
2745
+ {
2746
+ style: {
2747
+ width: THUMB,
2748
+ height: THUMB,
2749
+ borderRadius: theme.radii.sm,
2750
+ overflow: "hidden",
2751
+ borderWidth: active ? 2 : 0,
2752
+ borderColor: theme.colors.accent,
2753
+ backgroundColor: theme.colors.surface
2754
+ },
2755
+ children: image ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_skia.Canvas, { style: { width: THUMB, height: THUMB }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_skia.Image, { image, x: 0, y: 0, width: THUMB, height: THUMB, fit: "cover" }) }) : null
2756
+ }
2757
+ )
2758
+ ] });
2759
+ }
2760
+ function FilterStrip() {
2761
+ const { dispatch } = (0, import_core17.useEditor)();
2762
+ const luts = (0, import_core17.useLuts)();
2763
+ const theme = useTheme();
2764
+ const layer = useTargetImageLayer();
2765
+ const [intensityFor, setIntensityFor] = (0, import_react17.useState)(null);
2766
+ if (!layer) return null;
2767
+ const color = layer.data.color;
2768
+ const activeLut = color.lut;
2769
+ const applyLut = (lutId) => {
2770
+ if (lutId === null) {
2771
+ dispatch({ type: "layer/patchData", id: layer.id, patch: { color: { lut: void 0 } } });
2772
+ setIntensityFor(null);
2773
+ return;
2774
+ }
2775
+ if (activeLut?.id === lutId) {
2776
+ setIntensityFor((cur) => cur === lutId ? null : lutId);
2777
+ return;
2778
+ }
2779
+ dispatch({
2780
+ type: "layer/patchData",
2781
+ id: layer.id,
2782
+ patch: { color: { lut: { id: lutId, intensity: 1 } } }
2783
+ });
2784
+ };
2785
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native15.View, { style: { backgroundColor: theme.colors.background }, children: [
2786
+ intensityFor && activeLut?.id === intensityFor ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native15.View, { style: { paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2787
+ Slider,
2788
+ {
2789
+ value: activeLut.intensity,
2790
+ min: 0,
2791
+ max: 1,
2792
+ centered: false,
2793
+ valueLabel: `${Math.round(activeLut.intensity * 100)}`,
2794
+ onStart: () => dispatch({ type: "history/beginBatch", label: "intensity" }),
2795
+ onChange: (intensity) => dispatch({
2796
+ type: "layer/patchData",
2797
+ id: layer.id,
2798
+ patch: { color: { lut: { id: intensityFor, intensity } } }
2799
+ }),
2800
+ onEnd: () => dispatch({ type: "history/endBatch" })
2801
+ }
2802
+ ) }) : null,
2803
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
2804
+ import_react_native15.ScrollView,
2805
+ {
2806
+ horizontal: true,
2807
+ showsHorizontalScrollIndicator: false,
2808
+ contentContainerStyle: { gap: theme.spacing.sm, padding: theme.spacing.md },
2809
+ style: { flexGrow: 0 },
2810
+ children: [
2811
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native15.Pressable, { onPress: () => applyLut(null), children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Thumb, { lut: void 0, label: "Original", active: !activeLut }) }),
2812
+ luts.map((lut) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native15.Pressable, { onPress: () => applyLut(lut.id), children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2813
+ Thumb,
2814
+ {
2815
+ lut: { id: lut.id, intensity: 1 },
2816
+ label: lut.label,
2817
+ active: activeLut?.id === lut.id
2818
+ }
2819
+ ) }, lut.id))
2820
+ ]
2821
+ }
2822
+ )
2823
+ ] });
2824
+ }
2825
+
2826
+ // src/panels/LayoutPanel.tsx
2827
+ var import_core18 = require("@pictor-sdk/core");
2828
+ var import_react_native16 = require("react-native");
2829
+
2830
+ // src/panels/layoutMath.ts
2831
+ function layoutCells(preset, _doc) {
2832
+ switch (preset) {
2833
+ case "1":
2834
+ return [{ x: 0.5, y: 0.5, scale: 1 }];
2835
+ case "2v":
2836
+ return [
2837
+ { x: 0.5, y: 0.25, scale: 1 },
2838
+ { x: 0.5, y: 0.75, scale: 1 }
2839
+ ];
2840
+ case "2h":
2841
+ return [
2842
+ { x: 0.25, y: 0.5, scale: 0.5 },
2843
+ { x: 0.75, y: 0.5, scale: 0.5 }
2844
+ ];
2845
+ case "3v":
2846
+ return [
2847
+ { x: 0.5, y: 1 / 6, scale: 1 },
2848
+ { x: 0.5, y: 0.5, scale: 1 },
2849
+ { x: 0.5, y: 5 / 6, scale: 1 }
2850
+ ];
2851
+ case "4grid":
2852
+ return [
2853
+ { x: 0.25, y: 0.25, scale: 0.5 },
2854
+ { x: 0.75, y: 0.25, scale: 0.5 },
2855
+ { x: 0.25, y: 0.75, scale: 0.5 },
2856
+ { x: 0.75, y: 0.75, scale: 0.5 }
2857
+ ];
2858
+ default: {
2859
+ const _exhaustive = preset;
2860
+ void _exhaustive;
2861
+ return [{ x: 0.5, y: 0.5, scale: 1 }];
2862
+ }
2863
+ }
2864
+ }
2865
+ function cellToTransformPatch(cell) {
2866
+ return { x: cell.x, y: cell.y, scale: cell.scale, rotation: 0 };
2867
+ }
2868
+
2869
+ // src/panels/LayoutPanel.tsx
2870
+ var import_jsx_runtime19 = require("react/jsx-runtime");
2871
+ var PRESETS = [
2872
+ { id: "1", label: "Single" },
2873
+ { id: "2v", label: "2 rows" },
2874
+ { id: "2h", label: "2 cols" },
2875
+ { id: "3v", label: "3 rows" },
2876
+ { id: "4grid", label: "Grid" }
2877
+ ];
2878
+ function LayoutPanel() {
2879
+ const { document, dispatch } = (0, import_core18.useEditor)();
2880
+ const theme = useTheme();
2881
+ const apply = (preset) => {
2882
+ const cells = layoutCells(preset, document.size);
2883
+ const images = document.layers.filter((l) => l.type === "image");
2884
+ dispatch({ type: "history/beginBatch", label: "layout" });
2885
+ images.slice(0, cells.length).forEach((layer, i) => {
2886
+ dispatch({ type: "layer/transform", id: layer.id, patch: cellToTransformPatch(cells[i]) });
2887
+ });
2888
+ dispatch({ type: "history/endBatch" });
2889
+ };
2890
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2891
+ import_react_native16.ScrollView,
2892
+ {
2893
+ horizontal: true,
2894
+ showsHorizontalScrollIndicator: false,
2895
+ contentContainerStyle: { gap: theme.spacing.sm, padding: theme.spacing.md },
2896
+ style: { flexGrow: 0, backgroundColor: theme.colors.background },
2897
+ children: PRESETS.map((preset) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2898
+ import_react_native16.Pressable,
2899
+ {
2900
+ onPress: () => apply(preset.id),
2901
+ style: {
2902
+ paddingHorizontal: theme.spacing.md,
2903
+ paddingVertical: theme.spacing.sm,
2904
+ borderRadius: theme.radii.md,
2905
+ backgroundColor: theme.colors.surface
2906
+ },
2907
+ children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native16.Text, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm, fontWeight: theme.font.weight }, children: preset.label })
2908
+ },
2909
+ preset.id
2910
+ ))
2911
+ }
2912
+ );
2913
+ }
2914
+
2915
+ // src/panels/LuxPanel.tsx
2916
+ var import_core19 = require("@pictor-sdk/core");
2917
+ var import_skia8 = require("@pictor-sdk/skia");
2918
+ var import_react_native17 = require("react-native");
2919
+ var import_jsx_runtime20 = require("react/jsx-runtime");
2920
+ function LuxPanel() {
2921
+ const { dispatch } = (0, import_core19.useEditor)();
2922
+ const theme = useTheme();
2923
+ const layer = useTargetImageLayer();
2924
+ const data = layer?.data;
2925
+ const proposal = (0, import_skia8.useLuxAnalysis)(data?.source);
2926
+ if (!layer || !data) return null;
2927
+ const stored = data.color.lux;
2928
+ const intensity = stored?.intensity ?? 0;
2929
+ const state = (0, import_core19.luxControlState)(stored, proposal);
2930
+ const setIntensity = (value) => {
2931
+ const lux = (0, import_core19.luxIntensityPatch)(state, value);
2932
+ if (!lux) return;
2933
+ dispatch({ type: "layer/patchData", id: layer.id, patch: { color: { lux } } });
2934
+ };
2935
+ const reset = () => dispatch({
2936
+ type: "layer/patchData",
2937
+ id: layer.id,
2938
+ patch: { color: { lux: void 0 } }
2939
+ });
2940
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native17.View, { style: { backgroundColor: theme.colors.background, paddingBottom: theme.spacing.md }, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_react_native17.View, { style: { paddingHorizontal: theme.spacing.lg }, children: [
2941
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
2942
+ import_react_native17.View,
2943
+ {
2944
+ style: {
2945
+ flexDirection: "row",
2946
+ alignItems: "center",
2947
+ justifyContent: "center",
2948
+ gap: theme.spacing.sm,
2949
+ minHeight: 28
2950
+ },
2951
+ children: [
2952
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2953
+ import_react_native17.Text,
2954
+ {
2955
+ style: {
2956
+ color: theme.colors.textMuted,
2957
+ textAlign: "center",
2958
+ fontSize: theme.font.sizeSm
2959
+ },
2960
+ children: state.pending ? "Analyzing\u2026" : `Lux ${intensity === 0 ? "" : Math.round(intensity * 100)}`
2961
+ }
2962
+ ),
2963
+ intensity > 0 ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2964
+ import_react_native17.Pressable,
2965
+ {
2966
+ accessibilityRole: "button",
2967
+ accessibilityLabel: "Reset Lux",
2968
+ onPress: reset,
2969
+ hitSlop: 8,
2970
+ style: {
2971
+ paddingHorizontal: theme.spacing.sm,
2972
+ paddingVertical: 2,
2973
+ borderRadius: theme.radii.sm,
2974
+ borderWidth: 1,
2975
+ borderColor: theme.colors.track
2976
+ },
2977
+ children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native17.Text, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm }, children: "Reset" })
2978
+ }
2979
+ ) : null
2980
+ ]
2981
+ }
2982
+ ),
2983
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2984
+ Slider,
2985
+ {
2986
+ value: intensity,
2987
+ min: 0,
2988
+ max: 1,
2989
+ centered: false,
2990
+ disabled: !state.interactive,
2991
+ testID: "lux-slider",
2992
+ valueLabel: `${Math.round(intensity * 100)}`,
2993
+ onStart: () => dispatch({ type: "history/beginBatch", label: "lux" }),
2994
+ onChange: setIntensity,
2995
+ onEnd: () => dispatch({ type: "history/endBatch" })
2996
+ }
2997
+ )
2998
+ ] }) });
2999
+ }
3000
+
3001
+ // src/panels/MediaPanel.tsx
3002
+ var import_core20 = require("@pictor-sdk/core");
3003
+ var import_skia9 = require("@pictor-sdk/skia");
3004
+ var import_react_native18 = require("react-native");
3005
+ var import_jsx_runtime21 = require("react/jsx-runtime");
3006
+ function MediaPanel() {
3007
+ const { document, dispatch, profile } = (0, import_core20.useEditor)();
3008
+ const layer = useBaseMediaLayer();
3009
+ const theme = useTheme();
3010
+ const data = layer?.data;
3011
+ const resolved = (0, import_skia9.useResolvedImageSize)(data?.source);
3012
+ if (!layer) {
3013
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native18.View, { style: { padding: theme.spacing.lg }, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native18.Text, { style: { color: theme.colors.textMuted, fontSize: theme.font.sizeSm }, children: "This document has no base photo." }) });
3014
+ }
3015
+ const reset = () => {
3016
+ const config = (0, import_core20.mediaConfigFromProfile)(profile.interaction?.media);
3017
+ dispatch({
3018
+ type: "layer/transform",
3019
+ id: layer.id,
3020
+ patch: (0, import_core20.initialMediaTransform)(
3021
+ (0, import_skia9.preferResolvedSize)(resolved, data?.intrinsic),
3022
+ document.size,
3023
+ config
3024
+ )
3025
+ });
3026
+ };
3027
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_react_native18.View, { style: { padding: theme.spacing.lg, gap: theme.spacing.md }, children: [
3028
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native18.Text, { style: { color: theme.colors.textMuted, fontSize: theme.font.sizeSm }, children: "Drag to reposition \xB7 pinch to zoom \xB7 twist to rotate \xB7 double-tap to zoom" }),
3029
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3030
+ import_react_native18.Pressable,
3031
+ {
3032
+ accessibilityRole: "button",
3033
+ accessibilityLabel: "Reset photo placement",
3034
+ onPress: reset,
3035
+ style: {
3036
+ alignSelf: "flex-start",
3037
+ paddingHorizontal: theme.spacing.md,
3038
+ paddingVertical: theme.spacing.sm,
3039
+ borderRadius: theme.radii.md,
3040
+ backgroundColor: theme.colors.surface
3041
+ },
3042
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native18.Text, { style: { color: theme.colors.text, fontSize: theme.font.size }, children: "Reset" })
3043
+ }
3044
+ )
3045
+ ] });
3046
+ }
3047
+
3048
+ // src/panels/LayersPanel.tsx
3049
+ var import_core21 = require("@pictor-sdk/core");
3050
+ var import_react_native19 = require("react-native");
3051
+ var import_jsx_runtime22 = require("react/jsx-runtime");
3052
+ function LayersPanel() {
3053
+ const { document, selection, select, dispatch, profile } = (0, import_core21.useEditor)();
3054
+ const theme = useTheme();
3055
+ const layers = layersForPanel(document, profile.interaction?.layers);
3056
+ const minimumIndex = minimumLayerIndexForPanel(document, profile.interaction?.layers);
3057
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native19.ScrollView, { style: { maxHeight: 220, backgroundColor: theme.colors.background }, children: layers.map(({ layer, index }) => {
3058
+ const selected = selection.includes(layer.id);
3059
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
3060
+ import_react_native19.View,
3061
+ {
3062
+ style: {
3063
+ flexDirection: "row",
3064
+ alignItems: "center",
3065
+ paddingHorizontal: theme.spacing.md,
3066
+ paddingVertical: theme.spacing.xs,
3067
+ backgroundColor: selected ? theme.colors.surface : "transparent"
3068
+ },
3069
+ children: [
3070
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native19.Pressable, { style: { flex: 1 }, onPress: () => select([layer.id]), children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native19.Text, { style: { color: selected ? theme.colors.accent : theme.colors.text, fontSize: theme.font.sizeSm }, children: layer.name ?? `${layer.type} \xB7 ${layer.id.slice(0, 6)}` }) }),
3071
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
3072
+ import_react_native19.Pressable,
3073
+ {
3074
+ disabled: index === document.layers.length - 1,
3075
+ onPress: () => dispatch({ type: "layer/reorder", id: layer.id, to: index + 1 }),
3076
+ style: { padding: theme.spacing.xs },
3077
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native19.Text, { style: { color: theme.colors.textMuted }, children: "\u25B2" })
3078
+ }
3079
+ ),
3080
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
3081
+ import_react_native19.Pressable,
3082
+ {
3083
+ disabled: index === minimumIndex,
3084
+ onPress: () => dispatch({ type: "layer/reorder", id: layer.id, to: index - 1 }),
3085
+ style: { padding: theme.spacing.xs },
3086
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native19.Text, { style: { color: theme.colors.textMuted }, children: "\u25BC" })
3087
+ }
3088
+ ),
3089
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
3090
+ import_react_native19.Pressable,
3091
+ {
3092
+ onPress: () => dispatch({ type: "layer/remove", id: layer.id }),
3093
+ style: { padding: theme.spacing.xs },
3094
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native19.Text, { style: { color: theme.colors.danger }, children: "\u2715" })
3095
+ }
3096
+ )
3097
+ ]
3098
+ },
3099
+ layer.id
3100
+ );
3101
+ }) });
3102
+ }
3103
+ function minimumLayerIndexForPanel(document, interaction) {
3104
+ return interaction?.excludeBaseImage && document.layers[0]?.type === "image" ? 1 : 0;
3105
+ }
3106
+ function layersForPanel(document, interaction) {
3107
+ return document.layers.map((layer, index) => ({ layer, index })).filter(({ layer, index }) => (0, import_core21.isDirectManipulationLayer)(layer, index, interaction)).reverse();
3108
+ }
3109
+
3110
+ // src/panels/PerspectivePanel.tsx
3111
+ var import_core22 = require("@pictor-sdk/core");
3112
+ var import_react18 = require("react");
3113
+ var import_react_native20 = require("react-native");
3114
+ var import_jsx_runtime23 = require("react/jsx-runtime");
3115
+ var MAX_STRAIGHTEN_DEGREES = import_core22.MAX_PERSPECTIVE_DEGREES;
3116
+ var MODES = [
3117
+ { key: "vertical", label: "Vertical" },
3118
+ { key: "straighten", label: "Straighten" },
3119
+ { key: "horizontal", label: "Horizontal" }
3120
+ ];
3121
+ var toRadians = (degrees) => degrees * Math.PI / 180;
3122
+ var toDegrees = (radians) => radians * 180 / Math.PI;
3123
+ function PerspectivePanel() {
3124
+ const { dispatch } = (0, import_core22.useEditor)();
3125
+ const theme = useTheme();
3126
+ const layer = useTargetImageLayer();
3127
+ const [mode, setMode] = (0, import_react18.useState)("vertical");
3128
+ if (!layer) return null;
3129
+ const data = layer.data;
3130
+ const perspective = data.perspective ?? { vertical: 0, horizontal: 0 };
3131
+ const straightenDegrees = toDegrees(data.straighten ?? 0);
3132
+ const degreesOf = (key) => key === "straighten" ? straightenDegrees : perspective[key] * import_core22.MAX_PERSPECTIVE_DEGREES;
3133
+ const setDegrees = (key, degrees) => {
3134
+ if (key === "straighten") {
3135
+ return dispatch({
3136
+ type: "layer/patchData",
3137
+ id: layer.id,
3138
+ patch: { straighten: degrees === 0 ? void 0 : toRadians(degrees) }
3139
+ });
3140
+ }
3141
+ const next = { ...perspective, [key]: degrees / import_core22.MAX_PERSPECTIVE_DEGREES };
3142
+ dispatch({
3143
+ type: "layer/patchData",
3144
+ id: layer.id,
3145
+ // `perspectivePatch` deletes the key when both axes reach zero, so
3146
+ // straightening a correction back out leaves nothing serialized.
3147
+ patch: (0, import_core22.perspectivePatch)((0, import_core22.needsPerspective)(next) ? next : void 0)
3148
+ });
3149
+ };
3150
+ const value = degreesOf(mode);
3151
+ const max = mode === "straighten" ? MAX_STRAIGHTEN_DEGREES : import_core22.MAX_PERSPECTIVE_DEGREES;
3152
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_react_native20.View, { style: { backgroundColor: theme.colors.background, paddingBottom: theme.spacing.md }, children: [
3153
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
3154
+ import_react_native20.View,
3155
+ {
3156
+ style: {
3157
+ flexDirection: "row",
3158
+ justifyContent: "center",
3159
+ gap: theme.spacing.sm,
3160
+ paddingVertical: theme.spacing.sm
3161
+ },
3162
+ children: MODES.map(({ key, label }) => {
3163
+ const selected = key === mode;
3164
+ const degrees = degreesOf(key);
3165
+ const set = Math.abs(degrees) > 0.05;
3166
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
3167
+ import_react_native20.Pressable,
3168
+ {
3169
+ accessibilityRole: "button",
3170
+ accessibilityLabel: `${label} correction`,
3171
+ onPress: () => setMode(key),
3172
+ style: {
3173
+ flexDirection: "row",
3174
+ alignItems: "center",
3175
+ gap: 6,
3176
+ paddingHorizontal: theme.spacing.sm,
3177
+ paddingVertical: 6,
3178
+ borderRadius: theme.radii.sm,
3179
+ borderWidth: 1,
3180
+ borderColor: selected ? theme.colors.accent : theme.colors.track
3181
+ },
3182
+ children: [
3183
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
3184
+ import_react_native20.Text,
3185
+ {
3186
+ style: {
3187
+ color: selected ? theme.colors.accent : theme.colors.textMuted,
3188
+ fontSize: theme.font.sizeSm,
3189
+ fontWeight: theme.font.weight
3190
+ },
3191
+ children: set ? `${label} ${degrees.toFixed(1)}\xB0` : label
3192
+ }
3193
+ ),
3194
+ set ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
3195
+ import_react_native20.Pressable,
3196
+ {
3197
+ accessibilityRole: "button",
3198
+ accessibilityLabel: `Reset ${label.toLowerCase()}`,
3199
+ onPress: () => setDegrees(key, 0),
3200
+ hitSlop: 8,
3201
+ children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react_native20.Text, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm }, children: "\u2715" })
3202
+ }
3203
+ ) : null
3204
+ ]
3205
+ },
3206
+ key
3207
+ );
3208
+ })
3209
+ }
3210
+ ),
3211
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react_native20.View, { style: { paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
3212
+ Slider,
3213
+ {
3214
+ value,
3215
+ min: -max,
3216
+ max,
3217
+ centered: true,
3218
+ testID: `perspective-${mode}`,
3219
+ valueLabel: `${value > 0 ? "+" : ""}${value.toFixed(1)}\xB0`,
3220
+ onStart: () => dispatch({ type: "history/beginBatch", label: `perspective-${mode}` }),
3221
+ onChange: (v) => setDegrees(mode, v),
3222
+ onEnd: () => dispatch({ type: "history/endBatch" })
3223
+ }
3224
+ ) })
3225
+ ] });
3226
+ }
3227
+
3228
+ // src/panels/StickerPanel.tsx
3229
+ var import_core23 = require("@pictor-sdk/core");
3230
+ var import_react_native21 = require("react-native");
3231
+ var import_jsx_runtime24 = require("react/jsx-runtime");
3232
+ function StickerPanel() {
3233
+ const { dispatch, select } = (0, import_core23.useEditor)();
3234
+ const theme = useTheme();
3235
+ const [stickers] = useUiState("stickers", []);
3236
+ const add = (sticker) => {
3237
+ const id = (0, import_core23.createId)("sticker");
3238
+ dispatch({
3239
+ type: "layer/add",
3240
+ layer: {
3241
+ id,
3242
+ type: "sticker",
3243
+ transform: { x: 0.5, y: 0.5, scale: 1, rotation: 0 },
3244
+ opacity: 1,
3245
+ blendMode: "normal",
3246
+ data: {
3247
+ source: sticker.source,
3248
+ intrinsic: sticker.intrinsic ?? { width: 0, height: 0 }
3249
+ }
3250
+ }
3251
+ });
3252
+ select([id]);
3253
+ };
3254
+ if (stickers.length === 0) {
3255
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_react_native21.View, { style: { backgroundColor: theme.colors.background, padding: theme.spacing.md }, children: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_react_native21.Text, { style: { color: theme.colors.textMuted, textAlign: "center", fontSize: theme.font.sizeSm }, children: [
3256
+ "No stickers registered \u2014 pass `stickers` to ",
3257
+ "<Editor>",
3258
+ "."
3259
+ ] }) });
3260
+ }
3261
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3262
+ import_react_native21.ScrollView,
3263
+ {
3264
+ horizontal: true,
3265
+ showsHorizontalScrollIndicator: false,
3266
+ contentContainerStyle: { gap: theme.spacing.sm, padding: theme.spacing.md },
3267
+ style: { flexGrow: 0, backgroundColor: theme.colors.background },
3268
+ children: stickers.map((sticker) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_react_native21.Pressable, { onPress: () => add(sticker), children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3269
+ import_react_native21.Image,
3270
+ {
3271
+ source: sticker.source,
3272
+ style: { width: 64, height: 64, borderRadius: theme.radii.sm, backgroundColor: theme.colors.surface },
3273
+ resizeMode: "contain"
3274
+ }
3275
+ ) }, sticker.id))
3276
+ }
3277
+ );
3278
+ }
3279
+
3280
+ // src/panels/TiltShiftPanel.tsx
3281
+ var import_core24 = require("@pictor-sdk/core");
3282
+ var import_react_native22 = require("react-native");
3283
+ var import_jsx_runtime25 = require("react/jsx-runtime");
3284
+ var MODES2 = [
3285
+ { key: "off", label: "Off" },
3286
+ { key: "radial", label: "Radial" },
3287
+ { key: "linear", label: "Linear" }
3288
+ ];
3289
+ function TiltShiftPanel() {
3290
+ const { dispatch } = (0, import_core24.useEditor)();
3291
+ const theme = useTheme();
3292
+ const layer = useTargetImageLayer();
3293
+ if (!layer) return null;
3294
+ const data = layer.data;
3295
+ const tilt = (0, import_core24.needsTiltShift)(data.tiltShift) ? data.tiltShift : void 0;
3296
+ const mode = tilt?.mode ?? "off";
3297
+ const patch = (next) => dispatch({
3298
+ type: "layer/patchData",
3299
+ id: layer.id,
3300
+ patch: (0, import_core24.tiltShiftPatch)(next)
3301
+ });
3302
+ const setMode = (key) => {
3303
+ if (key === "off") return patch(void 0);
3304
+ patch(tilt ? { ...tilt, mode: key } : (0, import_core24.createTiltShift)(key));
3305
+ };
3306
+ const set = (over) => tilt && patch({ ...tilt, ...over });
3307
+ const sliders = [
3308
+ { key: "blur", label: "Blur", max: import_core24.MAX_TILT_BLUR },
3309
+ { key: "radius", label: "Size", max: import_core24.MAX_TILT_RADIUS },
3310
+ { key: "feather", label: "Feather", max: import_core24.MAX_TILT_FEATHER }
3311
+ ];
3312
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_react_native22.View, { style: { backgroundColor: theme.colors.background, paddingBottom: theme.spacing.md }, children: [
3313
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
3314
+ import_react_native22.View,
3315
+ {
3316
+ style: {
3317
+ flexDirection: "row",
3318
+ justifyContent: "center",
3319
+ gap: theme.spacing.md,
3320
+ paddingVertical: theme.spacing.sm
3321
+ },
3322
+ children: MODES2.map(({ key, label }) => {
3323
+ const selected = key === mode;
3324
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
3325
+ import_react_native22.Pressable,
3326
+ {
3327
+ accessibilityRole: "button",
3328
+ accessibilityLabel: `Tilt shift ${label.toLowerCase()}`,
3329
+ onPress: () => setMode(key),
3330
+ style: {
3331
+ paddingHorizontal: theme.spacing.md,
3332
+ paddingVertical: 6,
3333
+ borderRadius: theme.radii.sm,
3334
+ borderWidth: 1,
3335
+ borderColor: selected ? theme.colors.accent : theme.colors.track
3336
+ },
3337
+ children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
3338
+ import_react_native22.Text,
3339
+ {
3340
+ style: {
3341
+ color: selected ? theme.colors.accent : theme.colors.textMuted,
3342
+ fontSize: theme.font.sizeSm,
3343
+ fontWeight: theme.font.weight
3344
+ },
3345
+ children: label
3346
+ }
3347
+ )
3348
+ },
3349
+ key
3350
+ );
3351
+ })
3352
+ }
3353
+ ),
3354
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_react_native22.View, { style: { paddingHorizontal: theme.spacing.lg }, children: [
3355
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
3356
+ import_react_native22.Text,
3357
+ {
3358
+ style: {
3359
+ color: theme.colors.textMuted,
3360
+ textAlign: "center",
3361
+ fontSize: theme.font.sizeSm,
3362
+ minHeight: 20
3363
+ },
3364
+ children: tilt ? "Drag on the photo to place the focus" : "Off"
3365
+ }
3366
+ ),
3367
+ sliders.map(({ key, label, max }) => {
3368
+ const value = tilt?.[key] ?? 0;
3369
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_react_native22.View, { children: [
3370
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_react_native22.Text, { style: { color: theme.colors.textMuted, fontSize: theme.font.sizeSm }, children: [
3371
+ label,
3372
+ " ",
3373
+ tilt ? Math.round(value / max * 100) : ""
3374
+ ] }),
3375
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
3376
+ Slider,
3377
+ {
3378
+ value,
3379
+ min: 0,
3380
+ max,
3381
+ centered: false,
3382
+ testID: `tilt-${key}`,
3383
+ disabled: !tilt,
3384
+ onStart: () => dispatch({ type: "history/beginBatch", label: `tilt-${key}` }),
3385
+ onChange: (v) => set({ [key]: v }),
3386
+ onEnd: () => dispatch({ type: "history/endBatch" })
3387
+ }
3388
+ )
3389
+ ] }, key);
3390
+ })
3391
+ ] })
3392
+ ] });
3393
+ }
3394
+
3395
+ // src/ToolPanelHost.tsx
3396
+ var import_jsx_runtime26 = require("react/jsx-runtime");
3397
+ function resolvePanel(toolId, pluginPanel, builtins = BUILTIN_PANELS) {
3398
+ if (!toolId) return null;
3399
+ if (pluginPanel) return pluginPanel;
3400
+ return builtins[toolId] ?? null;
3401
+ }
3402
+ var BUILTIN_PANELS = {
3403
+ media: MediaPanel,
3404
+ addPhoto: AddPhotoPanel,
3405
+ filter: FilterStrip,
3406
+ adjust: AdjustPanel,
3407
+ lux: LuxPanel,
3408
+ color: ColorPanel,
3409
+ tilt: TiltShiftPanel,
3410
+ perspective: PerspectivePanel,
3411
+ crop: CropPanel,
3412
+ draw: DrawPanel,
3413
+ sticker: StickerPanel,
3414
+ layout: LayoutPanel,
3415
+ layers: LayersPanel
3416
+ };
3417
+ function ToolPanelHost() {
3418
+ const { activeTool, document, selection, dispatch, setActiveTool } = (0, import_core25.useEditor)();
3419
+ const tools = (0, import_core25.useTools)();
3420
+ const tool = tools.find((t) => t.id === activeTool);
3421
+ const Panel = resolvePanel(activeTool, tool?.Panel);
3422
+ const theme = useTheme();
3423
+ const finalized = (0, import_react19.useRef)(false);
3424
+ (0, import_react19.useEffect)(() => {
3425
+ if (!activeTool || !Panel) return;
3426
+ finalized.current = false;
3427
+ dispatch({ type: "history/beginBatch", label: activeTool });
3428
+ return () => {
3429
+ if (!finalized.current) dispatch({ type: "history/cancelBatch" });
3430
+ };
3431
+ }, [activeTool, Panel, dispatch]);
3432
+ if (!Panel) return null;
3433
+ const done = () => {
3434
+ finalized.current = true;
3435
+ dispatch({ type: "history/endBatch" });
3436
+ setActiveTool(null);
3437
+ };
3438
+ const cancel = () => {
3439
+ finalized.current = true;
3440
+ dispatch({ type: "history/cancelBatch" });
3441
+ setActiveTool(null);
3442
+ };
3443
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_react_native23.View, { style: { backgroundColor: theme.colors.background, borderTopLeftRadius: theme.radii.lg, borderTopRightRadius: theme.radii.lg, overflow: "hidden" }, children: [
3444
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(Panel, { document, selection, dispatch, close: done }),
3445
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_react_native23.View, { style: { height: 52, flexDirection: "row", alignItems: "center", justifyContent: "space-between", borderTopWidth: 1, borderTopColor: theme.colors.track, paddingHorizontal: theme.spacing.lg }, children: [
3446
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_react_native23.Pressable, { accessibilityRole: "button", accessibilityLabel: "Cancel edits", onPress: cancel, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_react_native23.Text, { style: { color: theme.colors.text, fontSize: theme.font.size }, children: "Cancel" }) }),
3447
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_react_native23.Text, { style: { color: theme.colors.text, fontSize: theme.font.size, fontWeight: theme.font.weight }, children: tool?.label ?? activeTool }),
3448
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_react_native23.Pressable, { accessibilityRole: "button", accessibilityLabel: "Apply edits", onPress: done, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_react_native23.Text, { style: { color: theme.colors.accent, fontSize: theme.font.size, fontWeight: theme.font.weight }, children: "Done" }) })
3449
+ ] })
3450
+ ] });
3451
+ }
3452
+
3453
+ // src/composer/TextComposer.tsx
3454
+ var import_core27 = require("@pictor-sdk/core");
3455
+ var import_react21 = require("react");
3456
+ var import_react_native24 = require("react-native");
3457
+
3458
+ // src/composer/keyboardLayout.ts
3459
+ var HIDDEN_KEYBOARD = { height: 0 };
3460
+ var MAX_DURATION = 400;
3461
+ var FALLBACK_DURATION = 180;
3462
+ function composerInsets(frame, safeAreaBottom = 0) {
3463
+ const height = Number.isFinite(frame.height) ? Math.max(0, frame.height) : 0;
3464
+ const visible = height > 0;
3465
+ const raw = frame.duration;
3466
+ const duration = raw === void 0 || !Number.isFinite(raw) || raw <= 0 ? FALLBACK_DURATION : Math.min(MAX_DURATION, raw);
3467
+ return {
3468
+ bottom: visible ? height : Math.max(0, safeAreaBottom),
3469
+ keyboardVisible: visible,
3470
+ duration
3471
+ };
3472
+ }
3473
+ function fitCanvasRect(container, document) {
3474
+ if (container.width <= 0 || container.height <= 0 || document.width <= 0 || document.height <= 0) {
3475
+ return { width: 0, height: 0, left: 0, top: 0 };
3476
+ }
3477
+ const scale = Math.min(container.width / document.width, container.height / document.height);
3478
+ const width = document.width * scale;
3479
+ const height = document.height * scale;
3480
+ return {
3481
+ width,
3482
+ height,
3483
+ left: (container.width - width) / 2,
3484
+ top: (container.height - height) / 2
3485
+ };
3486
+ }
3487
+
3488
+ // src/composer/useTextSession.ts
3489
+ var import_core26 = require("@pictor-sdk/core");
3490
+ var import_react20 = require("react");
3491
+ function useTextSession(options = {}) {
3492
+ const { document, dispatch, profile, selection, select } = (0, import_core26.useEditor)();
3493
+ const { layerId: requestedId, onFinished } = options;
3494
+ const [session] = (0, import_react20.useState)(
3495
+ () => (0, import_core26.resolveTextSession)(document.layers, requestedId ? [requestedId] : selection)
3496
+ );
3497
+ const [draftId, setDraftId] = (0, import_react20.useState)(null);
3498
+ const finished = (0, import_react20.useRef)(false);
3499
+ const priorSelection = (0, import_react20.useRef)([]);
3500
+ const capturedPrior = (0, import_react20.useRef)(false);
3501
+ if (!capturedPrior.current) {
3502
+ capturedPrior.current = true;
3503
+ priorSelection.current = [...selection];
3504
+ }
3505
+ const latest = (0, import_react20.useRef)({ document, dispatch, select, selection, profile });
3506
+ latest.current = { document, dispatch, select, selection, profile };
3507
+ (0, import_react20.useEffect)(() => {
3508
+ latest.current.dispatch({ type: "history/beginBatch", label: "text" });
3509
+ if (session.kind === "draft") {
3510
+ const draft = (0, import_core26.createTextDraft)(latest.current.profile);
3511
+ setDraftId(draft.id);
3512
+ latest.current.dispatch({ type: "layer/add", layer: draft });
3513
+ latest.current.select([draft.id]);
3514
+ } else if (session.layerId) {
3515
+ const layer2 = latest.current.document.layers.find((l) => l.id === session.layerId);
3516
+ const layerData = layer2?.data;
3517
+ if (layerData && (0, import_core26.needsSpanCollapse)(layerData)) {
3518
+ latest.current.dispatch({
3519
+ type: "layer/patchData",
3520
+ id: session.layerId,
3521
+ patch: { spans: (0, import_core26.collapseTextSpans)(layerData).spans }
3522
+ });
3523
+ }
3524
+ }
3525
+ return () => {
3526
+ if (finished.current) return;
3527
+ finished.current = true;
3528
+ latest.current.dispatch({ type: "history/cancelBatch" });
3529
+ latest.current.select(priorSelection.current);
3530
+ };
3531
+ }, []);
3532
+ const layerId = session.kind === "existing" ? session.layerId : draftId;
3533
+ const layer = layerId ? document.layers.find((l) => l.id === layerId) : void 0;
3534
+ const data = layer ? layer.data : void 0;
3535
+ const span = data ? (0, import_core26.textSpanOf)(data) : void 0;
3536
+ const value = data ? (0, import_core26.textValueOf)(data) : "";
3537
+ const idRef = (0, import_react20.useRef)(null);
3538
+ idRef.current = layerId ?? null;
3539
+ const dataRef = (0, import_react20.useRef)(void 0);
3540
+ dataRef.current = data;
3541
+ const patchData = (0, import_react20.useCallback)((patch) => {
3542
+ const id = idRef.current;
3543
+ if (!id) return;
3544
+ latest.current.dispatch({ type: "layer/patchData", id, patch });
3545
+ }, []);
3546
+ const setValue = (0, import_react20.useCallback)(
3547
+ (next) => {
3548
+ const current = dataRef.current;
3549
+ if (!current) return;
3550
+ patchData({ spans: (0, import_core26.patchTextSpan)(current, { text: next }).spans });
3551
+ },
3552
+ [patchData]
3553
+ );
3554
+ const styleSpan = (0, import_react20.useCallback)(
3555
+ (patch) => {
3556
+ const current = dataRef.current;
3557
+ if (!current) return;
3558
+ patchData({ spans: (0, import_core26.patchTextSpan)(current, patch).spans });
3559
+ },
3560
+ [patchData]
3561
+ );
3562
+ const setSize = (0, import_react20.useCallback)(
3563
+ (size) => styleSpan({ fontSize: (0, import_core26.clampTextSize)(size) }),
3564
+ [styleSpan]
3565
+ );
3566
+ const commit = (0, import_react20.useCallback)(() => {
3567
+ if (finished.current) return null;
3568
+ finished.current = true;
3569
+ const id = idRef.current;
3570
+ const blank = (0, import_core26.isBlankText)(dataRef.current ? (0, import_core26.textValueOf)(dataRef.current) : "");
3571
+ if (session.kind === "draft" && blank) {
3572
+ latest.current.dispatch({ type: "history/cancelBatch" });
3573
+ latest.current.select(priorSelection.current);
3574
+ onFinished?.(null);
3575
+ return null;
3576
+ }
3577
+ latest.current.dispatch({ type: "history/endBatch" });
3578
+ onFinished?.(id ?? null);
3579
+ return id ?? null;
3580
+ }, [session.kind, onFinished]);
3581
+ const cancel = (0, import_react20.useCallback)(() => {
3582
+ if (finished.current) return;
3583
+ finished.current = true;
3584
+ latest.current.dispatch({ type: "history/cancelBatch" });
3585
+ latest.current.select(priorSelection.current);
3586
+ onFinished?.(null);
3587
+ }, [onFinished]);
3588
+ return (0, import_react20.useMemo)(
3589
+ () => ({
3590
+ layer,
3591
+ kind: session.kind,
3592
+ value,
3593
+ setValue,
3594
+ span,
3595
+ data,
3596
+ styleSpan,
3597
+ styleText: patchData,
3598
+ setSize,
3599
+ commit,
3600
+ cancel
3601
+ }),
3602
+ [layer, session.kind, value, setValue, span, data, styleSpan, patchData, setSize, commit, cancel]
3603
+ );
3604
+ }
3605
+
3606
+ // src/composer/TextComposer.tsx
3607
+ var import_jsx_runtime27 = require("react/jsx-runtime");
3608
+ var SWATCHES2 = ["#ffffff", "#000000", "#ed4956", "#ffe25c", "#78de85", "#4dc4ff", "#a15fed"];
3609
+ var ALIGNS = ["left", "center", "right"];
3610
+ var ALIGN_GLYPH = { left: "\u21E4", center: "\u2630", right: "\u21E5" };
3611
+ var FILL_MODES = [void 0, "solid", "perLine"];
3612
+ var STROKE_WIDTHS = [void 0, 2e-3, 5e-3];
3613
+ var SHADOWS = [
3614
+ void 0,
3615
+ { color: "#000000", blur: 0.012, dx: 2e-3, dy: 4e-3 },
3616
+ { color: "#000000", blur: 2e-3, dx: 6e-3, dy: 8e-3 }
3617
+ ];
3618
+ function TextComposer({ session, insets, onDone, onCancel }) {
3619
+ const theme = useTheme();
3620
+ const store = (0, import_core27.useEditorStore)();
3621
+ const { document } = (0, import_core27.useEditor)();
3622
+ const [container, setContainer] = (0, import_react21.useState)({ width: 0, height: 0 });
3623
+ const input = (0, import_react21.useRef)(null);
3624
+ const [selectionRange, setSelectionRange] = (0, import_react21.useState)(
3625
+ void 0
3626
+ );
3627
+ const data = session.data;
3628
+ const span = session.span;
3629
+ const layout = (0, import_react21.useMemo)(() => {
3630
+ const canvas = fitCanvasRect(container, document.size);
3631
+ const fontSize = (span?.fontSize ?? 0.08) * canvas.width;
3632
+ const centreY = canvas.top + (session.layer?.transform.y ?? 0.42) * canvas.height;
3633
+ const height = canvas.height;
3634
+ return {
3635
+ fontSize: Math.max(1, fontSize),
3636
+ lineHeight: Math.max(1, fontSize * (span?.lineHeight ?? 1.2)),
3637
+ left: canvas.left,
3638
+ width: Math.max(1, canvas.width),
3639
+ top: Math.max(0, centreY - height / 2),
3640
+ height: Math.max(1, height)
3641
+ };
3642
+ }, [container, document.size, span?.fontSize, span?.lineHeight, session.layer?.transform.y]);
3643
+ const fonts = (0, import_react21.useMemo)(() => (0, import_core27.fontChoices)(store.registry.fonts), [store.registry.fonts]);
3644
+ (0, import_react21.useEffect)(() => {
3645
+ const handle = setTimeout(() => input.current?.focus(), 50);
3646
+ return () => clearTimeout(handle);
3647
+ }, []);
3648
+ const onSelectionChange = (0, import_react21.useCallback)(
3649
+ (event) => {
3650
+ setSelectionRange(event.nativeEvent.selection);
3651
+ },
3652
+ []
3653
+ );
3654
+ const keepFocus = (0, import_react21.useCallback)(
3655
+ (apply) => {
3656
+ apply();
3657
+ if (!input.current?.isFocused()) input.current?.focus();
3658
+ if (selectionRange) {
3659
+ const range = selectionRange;
3660
+ requestAnimationFrame(() => input.current?.setNativeProps({ selection: range }));
3661
+ }
3662
+ },
3663
+ [selectionRange]
3664
+ );
3665
+ const cycleAlign = () => {
3666
+ if (!data) return;
3667
+ const next = ALIGNS[(ALIGNS.indexOf(data.align) + 1) % ALIGNS.length];
3668
+ keepFocus(() => session.styleText({ align: next }));
3669
+ };
3670
+ const cycleFill = () => {
3671
+ if (!data || !span) return;
3672
+ const index = FILL_MODES.indexOf(data.fill?.mode);
3673
+ const next = FILL_MODES[(index + 1) % FILL_MODES.length];
3674
+ keepFocus(
3675
+ () => session.styleText({
3676
+ fill: next ? {
3677
+ color: span.color === "#000000" ? "#ffffff" : "#000000",
3678
+ radius: 0.012,
3679
+ padding: 0.012,
3680
+ mode: next
3681
+ } : void 0
3682
+ })
3683
+ );
3684
+ };
3685
+ const cycleStroke = () => {
3686
+ if (!data || !span) return;
3687
+ const current = data.stroke?.width;
3688
+ const index = STROKE_WIDTHS.findIndex((w) => w === current);
3689
+ const next = STROKE_WIDTHS[(index + 1) % STROKE_WIDTHS.length];
3690
+ keepFocus(
3691
+ () => session.styleText({
3692
+ stroke: next ? { color: span.color === "#000000" ? "#ffffff" : "#000000", width: next } : void 0
3693
+ })
3694
+ );
3695
+ };
3696
+ const cycleShadow = () => {
3697
+ if (!data) return;
3698
+ const current = data.shadow;
3699
+ const index = current ? SHADOWS.findIndex((s) => s && current && s.blur === current.blur && s.dx === current.dx) : 0;
3700
+ const next = SHADOWS[(index + 1) % SHADOWS.length];
3701
+ keepFocus(() => session.styleText({ shadow: next ? { ...next } : void 0 }));
3702
+ };
3703
+ (0, import_react21.useEffect)(() => {
3704
+ const subscription = import_react_native24.BackHandler.addEventListener("hardwareBackPress", () => {
3705
+ if (import_react_native24.Keyboard.isVisible?.() ?? insets.keyboardVisible) {
3706
+ import_react_native24.Keyboard.dismiss();
3707
+ return true;
3708
+ }
3709
+ onCancel();
3710
+ return true;
3711
+ });
3712
+ return () => subscription.remove();
3713
+ }, [onCancel, insets.keyboardVisible]);
3714
+ const controlRow = {
3715
+ flexDirection: "row",
3716
+ alignItems: "center",
3717
+ gap: theme.spacing.sm,
3718
+ paddingHorizontal: theme.spacing.md
3719
+ };
3720
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
3721
+ import_react_native24.View,
3722
+ {
3723
+ style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0 },
3724
+ onLayout: (event) => {
3725
+ const { width, height } = event.nativeEvent.layout;
3726
+ setContainer(
3727
+ (current) => current.width === width && current.height === height ? current : { width, height }
3728
+ );
3729
+ },
3730
+ children: [
3731
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3732
+ import_react_native24.View,
3733
+ {
3734
+ pointerEvents: "none",
3735
+ style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0, backgroundColor: "#00000055" }
3736
+ }
3737
+ ),
3738
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3739
+ import_react_native24.TextInput,
3740
+ {
3741
+ ref: input,
3742
+ value: session.value,
3743
+ onChangeText: session.setValue,
3744
+ onSelectionChange,
3745
+ multiline: true,
3746
+ submitBehavior: "newline",
3747
+ autoCorrect: false,
3748
+ spellCheck: false,
3749
+ autoComplete: "off",
3750
+ importantForAutofill: "no",
3751
+ accessibilityLabel: "Story text",
3752
+ placeholder: "",
3753
+ style: {
3754
+ position: "absolute",
3755
+ left: layout.left,
3756
+ width: layout.width,
3757
+ top: layout.top,
3758
+ height: layout.height,
3759
+ // Fully transparent glyphs. The Skia layer is what the user reads;
3760
+ // drawing the same text twice would double it on screen. Written as
3761
+ // rgba rather than the 'transparent' keyword because the keyword is
3762
+ // not reliably honoured for TextInput colour on Android.
3763
+ color: "rgba(0,0,0,0)",
3764
+ fontSize: layout.fontSize,
3765
+ lineHeight: layout.lineHeight,
3766
+ textAlign: data?.align ?? "center",
3767
+ ...import_react_native24.Platform.OS === "ios" ? {} : { textAlignVertical: "center" },
3768
+ padding: 0
3769
+ },
3770
+ selectionColor: theme.colors.accent,
3771
+ cursorColor: theme.colors.accent
3772
+ }
3773
+ ),
3774
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
3775
+ import_react_native24.View,
3776
+ {
3777
+ style: {
3778
+ position: "absolute",
3779
+ left: 0,
3780
+ right: 0,
3781
+ top: 0,
3782
+ height: 56,
3783
+ flexDirection: "row",
3784
+ alignItems: "center",
3785
+ justifyContent: "space-between",
3786
+ paddingHorizontal: theme.spacing.lg
3787
+ },
3788
+ children: [
3789
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react_native24.Pressable, { accessibilityRole: "button", accessibilityLabel: "Cancel text", onPress: onCancel, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react_native24.Text, { style: { color: theme.colors.text, fontSize: theme.font.size }, children: "Cancel" }) }),
3790
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react_native24.Pressable, { accessibilityRole: "button", accessibilityLabel: "Done editing text", onPress: onDone, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3791
+ import_react_native24.Text,
3792
+ {
3793
+ style: {
3794
+ color: theme.colors.accent,
3795
+ fontSize: theme.font.size,
3796
+ fontWeight: theme.font.weight
3797
+ },
3798
+ children: "Done"
3799
+ }
3800
+ ) })
3801
+ ]
3802
+ }
3803
+ ),
3804
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
3805
+ import_react_native24.View,
3806
+ {
3807
+ style: {
3808
+ position: "absolute",
3809
+ left: 0,
3810
+ right: 0,
3811
+ bottom: insets.bottom,
3812
+ paddingVertical: theme.spacing.sm,
3813
+ gap: theme.spacing.sm,
3814
+ backgroundColor: "#00000066"
3815
+ },
3816
+ children: [
3817
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_react_native24.View, { style: controlRow, children: [
3818
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3819
+ import_react_native24.Text,
3820
+ {
3821
+ style: { color: theme.colors.textMuted, fontSize: theme.font.sizeSm, width: 34 },
3822
+ accessibilityLabel: "Text size",
3823
+ children: "Aa"
3824
+ }
3825
+ ),
3826
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react_native24.View, { style: { flex: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3827
+ Slider,
3828
+ {
3829
+ value: span?.fontSize ?? import_core27.MIN_TEXT_SIZE,
3830
+ min: import_core27.MIN_TEXT_SIZE,
3831
+ max: import_core27.MAX_TEXT_SIZE,
3832
+ centered: false,
3833
+ onChange: (size) => session.setSize(size)
3834
+ }
3835
+ ) }),
3836
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3837
+ import_react_native24.Pressable,
3838
+ {
3839
+ accessibilityRole: "button",
3840
+ accessibilityLabel: `Alignment ${data?.align ?? "center"}`,
3841
+ onPress: cycleAlign,
3842
+ style: { padding: theme.spacing.sm },
3843
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react_native24.Text, { style: { color: theme.colors.text, fontWeight: theme.font.weight }, children: ALIGN_GLYPH[data?.align ?? "center"] })
3844
+ }
3845
+ ),
3846
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3847
+ import_react_native24.Pressable,
3848
+ {
3849
+ accessibilityRole: "button",
3850
+ accessibilityLabel: "Text background",
3851
+ accessibilityState: { selected: Boolean(data?.fill) },
3852
+ onPress: cycleFill,
3853
+ style: { padding: theme.spacing.sm },
3854
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3855
+ import_react_native24.Text,
3856
+ {
3857
+ style: {
3858
+ color: data?.fill ? theme.colors.accent : theme.colors.text,
3859
+ fontWeight: theme.font.weight
3860
+ },
3861
+ children: data?.fill?.mode === "perLine" ? "A\u20DE\u20DE" : "A\u20DE"
3862
+ }
3863
+ )
3864
+ }
3865
+ ),
3866
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3867
+ import_react_native24.Pressable,
3868
+ {
3869
+ accessibilityRole: "button",
3870
+ accessibilityLabel: "Text outline",
3871
+ accessibilityState: { selected: Boolean(data?.stroke) },
3872
+ onPress: cycleStroke,
3873
+ style: { padding: theme.spacing.sm },
3874
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3875
+ import_react_native24.Text,
3876
+ {
3877
+ style: {
3878
+ color: data?.stroke ? theme.colors.accent : theme.colors.text,
3879
+ fontWeight: theme.font.weight
3880
+ },
3881
+ children: "\u24B6"
3882
+ }
3883
+ )
3884
+ }
3885
+ ),
3886
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3887
+ import_react_native24.Pressable,
3888
+ {
3889
+ accessibilityRole: "button",
3890
+ accessibilityLabel: "Text shadow",
3891
+ accessibilityState: { selected: Boolean(data?.shadow) },
3892
+ onPress: cycleShadow,
3893
+ style: { padding: theme.spacing.sm },
3894
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3895
+ import_react_native24.Text,
3896
+ {
3897
+ style: {
3898
+ color: data?.shadow ? theme.colors.accent : theme.colors.text,
3899
+ fontWeight: theme.font.weight
3900
+ },
3901
+ children: "A\u02D9"
3902
+ }
3903
+ )
3904
+ }
3905
+ )
3906
+ ] }),
3907
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3908
+ import_react_native24.ScrollView,
3909
+ {
3910
+ horizontal: true,
3911
+ keyboardShouldPersistTaps: "always",
3912
+ showsHorizontalScrollIndicator: false,
3913
+ contentContainerStyle: { gap: theme.spacing.sm, paddingHorizontal: theme.spacing.md, alignItems: "center" },
3914
+ style: { flexGrow: 0 },
3915
+ children: fonts.map((font) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3916
+ import_react_native24.Pressable,
3917
+ {
3918
+ accessibilityRole: "button",
3919
+ accessibilityLabel: `Font ${font.label}`,
3920
+ accessibilityState: { selected: span?.fontFamily === font.family },
3921
+ onPress: () => keepFocus(() => session.styleSpan({ fontFamily: font.family })),
3922
+ style: {
3923
+ paddingHorizontal: theme.spacing.md,
3924
+ paddingVertical: theme.spacing.xs,
3925
+ borderRadius: theme.radii.md,
3926
+ backgroundColor: span?.fontFamily === font.family ? theme.colors.surface : "transparent",
3927
+ borderWidth: 1,
3928
+ borderColor: span?.fontFamily === font.family ? theme.colors.accent : theme.colors.track
3929
+ },
3930
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react_native24.Text, { style: { color: theme.colors.text, fontSize: theme.font.sizeSm }, children: font.label })
3931
+ },
3932
+ font.family
3933
+ ))
3934
+ }
3935
+ ),
3936
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3937
+ import_react_native24.ScrollView,
3938
+ {
3939
+ horizontal: true,
3940
+ keyboardShouldPersistTaps: "always",
3941
+ showsHorizontalScrollIndicator: false,
3942
+ contentContainerStyle: { gap: theme.spacing.sm, paddingHorizontal: theme.spacing.md, alignItems: "center" },
3943
+ style: { flexGrow: 0 },
3944
+ children: SWATCHES2.map((color) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3945
+ import_react_native24.Pressable,
3946
+ {
3947
+ accessibilityRole: "button",
3948
+ accessibilityLabel: `Text colour ${color}`,
3949
+ accessibilityState: { selected: span?.color === color },
3950
+ onPress: () => keepFocus(() => session.styleSpan({ color })),
3951
+ style: {
3952
+ width: 28,
3953
+ height: 28,
3954
+ borderRadius: 14,
3955
+ backgroundColor: color,
3956
+ borderWidth: span?.color === color ? 3 : 1,
3957
+ borderColor: span?.color === color ? theme.colors.accent : theme.colors.track
3958
+ }
3959
+ },
3960
+ color
3961
+ ))
3962
+ }
3963
+ )
3964
+ ]
3965
+ }
3966
+ )
3967
+ ]
3968
+ }
3969
+ );
3970
+ }
3971
+ function TextComposerHost({
3972
+ Composer = TextComposer
3973
+ }) {
3974
+ const { setActiveTool } = (0, import_core27.useEditor)();
3975
+ const session = useTextSession();
3976
+ const [frame, setFrame] = (0, import_react21.useState)(HIDDEN_KEYBOARD);
3977
+ const token = (0, import_react21.useRef)(0);
3978
+ (0, import_react21.useEffect)(() => {
3979
+ token.current += 1;
3980
+ const mine = token.current;
3981
+ const show = import_react_native24.Keyboard.addListener(
3982
+ import_react_native24.Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow",
3983
+ (event) => {
3984
+ if (mine !== token.current) return;
3985
+ setFrame({ height: event.endCoordinates?.height ?? 0, duration: event.duration });
3986
+ }
3987
+ );
3988
+ const hide = import_react_native24.Keyboard.addListener(
3989
+ import_react_native24.Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide",
3990
+ (event) => {
3991
+ if (mine !== token.current) return;
3992
+ setFrame({ height: 0, duration: event?.duration });
3993
+ }
3994
+ );
3995
+ return () => {
3996
+ token.current += 1;
3997
+ show.remove();
3998
+ hide.remove();
3999
+ };
4000
+ }, []);
4001
+ const insets = (0, import_react21.useMemo)(() => composerInsets(frame), [frame]);
4002
+ const finish = (0, import_react21.useCallback)(
4003
+ (commit) => {
4004
+ if (commit) session.commit();
4005
+ else session.cancel();
4006
+ import_react_native24.Keyboard.dismiss();
4007
+ setActiveTool(null);
4008
+ },
4009
+ [session, setActiveTool]
4010
+ );
4011
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
4012
+ Composer,
4013
+ {
4014
+ session,
4015
+ insets,
4016
+ onDone: () => finish(true),
4017
+ onCancel: () => finish(false)
4018
+ }
4019
+ );
4020
+ }
4021
+
4022
+ // src/components/TopBar.tsx
4023
+ var import_core28 = require("@pictor-sdk/core");
4024
+ var import_react_native25 = require("react-native");
4025
+ var import_jsx_runtime28 = require("react/jsx-runtime");
4026
+ function TopBar({ onCancel, onNext, busy = false }) {
4027
+ const { canUndo, canRedo, dispatch } = (0, import_core28.useEditor)();
4028
+ const theme = useTheme();
4029
+ const button = (label, enabled, onPress, color) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native25.Pressable, { disabled: !enabled, onPress, style: { padding: theme.spacing.sm }, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
4030
+ import_react_native25.Text,
4031
+ {
4032
+ style: {
4033
+ color: enabled ? color ?? theme.colors.text : theme.colors.textMuted,
4034
+ fontSize: theme.font.size,
4035
+ fontWeight: theme.font.weight
4036
+ },
4037
+ children: label
4038
+ }
4039
+ ) });
4040
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
4041
+ import_react_native25.View,
4042
+ {
4043
+ style: {
4044
+ flexDirection: "row",
4045
+ alignItems: "center",
4046
+ justifyContent: "space-between",
4047
+ paddingHorizontal: theme.spacing.sm,
4048
+ backgroundColor: theme.colors.background
4049
+ },
4050
+ children: [
4051
+ button("Cancel", !!onCancel, onCancel),
4052
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react_native25.View, { style: { flexDirection: "row" }, children: [
4053
+ button("\u21BA", canUndo, () => dispatch({ type: "history/undo" })),
4054
+ button("\u21BB", canRedo, () => dispatch({ type: "history/redo" }))
4055
+ ] }),
4056
+ busy ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native25.ActivityIndicator, { color: theme.colors.accent, style: { padding: theme.spacing.sm } }) : button("Next", !!onNext, onNext, theme.colors.accent)
4057
+ ]
4058
+ }
4059
+ );
4060
+ }
4061
+
4062
+ // src/components/ToolRail.tsx
4063
+ var import_core29 = require("@pictor-sdk/core");
4064
+ var import_react_native26 = require("react-native");
4065
+ var import_jsx_runtime29 = require("react/jsx-runtime");
4066
+ function ToolRail() {
4067
+ const { activeTool, setActiveTool, document, selection, dispatch } = (0, import_core29.useEditor)();
4068
+ const tools = (0, import_core29.useTools)();
4069
+ const theme = useTheme();
4070
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
4071
+ import_react_native26.ScrollView,
4072
+ {
4073
+ horizontal: true,
4074
+ showsHorizontalScrollIndicator: false,
4075
+ contentContainerStyle: {
4076
+ gap: theme.spacing.md,
4077
+ paddingHorizontal: theme.spacing.md,
4078
+ paddingVertical: theme.spacing.md
4079
+ },
4080
+ style: { flexGrow: 0, backgroundColor: theme.colors.background },
4081
+ children: tools.map((tool) => {
4082
+ const enabled = tool.isEnabled ? tool.isEnabled({ document, selection }) : true;
4083
+ const active = activeTool === tool.id;
4084
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
4085
+ import_react_native26.Pressable,
4086
+ {
4087
+ disabled: !enabled,
4088
+ onPress: () => {
4089
+ if (tool.onActivate) {
4090
+ tool.onActivate({ document, dispatch });
4091
+ } else {
4092
+ setActiveTool(active ? null : tool.id);
4093
+ }
4094
+ },
4095
+ style: { width: 72, alignItems: "center", gap: 6, opacity: enabled ? 1 : 0.4 },
4096
+ children: [
4097
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_react_native26.View, { style: { width: 46, height: 46, borderRadius: 23, borderWidth: 1, borderColor: active ? theme.colors.accent : theme.colors.textMuted, backgroundColor: active ? theme.colors.surface : "transparent", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_react_native26.Text, { style: { color: active ? theme.colors.accent : theme.colors.text, fontSize: 18 }, children: tool.label.slice(0, 1) }) }),
4098
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
4099
+ import_react_native26.Text,
4100
+ {
4101
+ style: {
4102
+ color: active ? theme.colors.accent : theme.colors.text,
4103
+ fontSize: theme.font.sizeSm,
4104
+ fontWeight: theme.font.weight
4105
+ },
4106
+ numberOfLines: 1,
4107
+ children: tool.label
4108
+ }
4109
+ )
4110
+ ]
4111
+ },
4112
+ tool.id
4113
+ );
4114
+ })
4115
+ }
4116
+ );
4117
+ }
4118
+
4119
+ // src/Editor.tsx
4120
+ var import_jsx_runtime30 = require("react/jsx-runtime");
4121
+ function CanvasStage({ children, onError }) {
4122
+ const { activeTool, profile } = (0, import_core30.useEditor)();
4123
+ const { previews } = useInteractionState();
4124
+ const surface = resolveGestureSurface(activeTool);
4125
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
4126
+ import_skia10.EditorCanvas,
4127
+ {
4128
+ style: { flex: 1 },
4129
+ onError,
4130
+ previewTransforms: previews,
4131
+ showSelectionChrome: (0, import_core30.selectionChromeEnabled)(profile),
4132
+ children: [
4133
+ surface === "draw" ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(DrawingSurface, {}) : surface === "crop" ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
4134
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(CropGestureSurface, {}),
4135
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(CropGrid, {})
4136
+ ] }) : surface === "tilt" ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(TiltShiftGestureSurface, {}) : surface === "media" ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(StoryMediaGestureSurface, {}) : /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
4137
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(LayerGestureSurface, {}),
4138
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(InteractionGuides, {})
4139
+ ] }),
4140
+ children
4141
+ ]
4142
+ }
4143
+ );
4144
+ }
4145
+ function mergeSlots(overrides) {
4146
+ return {
4147
+ TopBar,
4148
+ CanvasStage,
4149
+ ToolRail,
4150
+ ToolPanelHost,
4151
+ TextComposer,
4152
+ ...overrides
4153
+ };
4154
+ }
4155
+ function EditorBody({
4156
+ slots,
4157
+ onExport,
4158
+ onCancel,
4159
+ onError,
4160
+ exportSpec,
4161
+ stickers
4162
+ }) {
4163
+ const { export: exportDoc, activeTool } = (0, import_core30.useEditor)();
4164
+ const { layerDragActive } = useInteractionState();
4165
+ const [, setStickers] = useUiState("stickers", []);
4166
+ import_react22.default.useEffect(() => {
4167
+ if (stickers) setStickers(stickers);
4168
+ }, [stickers, setStickers]);
4169
+ const theme = useTheme();
4170
+ const [busy, setBusy] = (0, import_react22.useState)(false);
4171
+ const handleNext = onExport ? async () => {
4172
+ setBusy(true);
4173
+ try {
4174
+ const bytes = await exportDoc(exportSpec);
4175
+ await onExport(bytes);
4176
+ } catch (error) {
4177
+ onError?.(error);
4178
+ } finally {
4179
+ setBusy(false);
4180
+ }
4181
+ } : void 0;
4182
+ const {
4183
+ TopBar: Top,
4184
+ CanvasStage: Stage,
4185
+ ToolRail: Rail,
4186
+ ToolPanelHost: PanelHost,
4187
+ TextComposer: Composer
4188
+ } = slots;
4189
+ const composing = activeTool === "text" && Composer !== null;
4190
+ const chrome = !layerDragActive;
4191
+ const hidden = { opacity: chrome ? 1 : 0 };
4192
+ const hits = chrome ? "auto" : "none";
4193
+ return (
4194
+ // GestureDetector requires a GestureHandlerRootView ancestor; providing it
4195
+ // here means consumers never have to know (nesting under an app-level one
4196
+ // is harmless).
4197
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_react_native_gesture_handler8.GestureHandlerRootView, { style: { flex: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_react_native27.View, { style: { flex: 1, backgroundColor: theme.colors.background }, children: [
4198
+ Top && !composing ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_react_native27.View, { style: hidden, pointerEvents: hits, children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(Top, { onCancel, onNext: handleNext, busy }) }) : null,
4199
+ Stage ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(Stage, { onError }) : null,
4200
+ PanelHost && !composing ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_react_native27.View, { style: hidden, pointerEvents: hits, children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(PanelHost, {}) }) : null,
4201
+ Rail && !activeTool ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_react_native27.View, { style: hidden, pointerEvents: hits, children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(Rail, {}) }) : null,
4202
+ composing && Composer ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(TextComposerHost, { Composer }) : null
4203
+ ] }) })
4204
+ );
4205
+ }
4206
+ function Editor({
4207
+ profile,
4208
+ plugins,
4209
+ source,
4210
+ intrinsic,
4211
+ initialDocument,
4212
+ onExport,
4213
+ onCancel,
4214
+ onError,
4215
+ exportSpec,
4216
+ slots,
4217
+ theme,
4218
+ stickers,
4219
+ requestImages
4220
+ }) {
4221
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
4222
+ import_core30.EditorProvider,
4223
+ {
4224
+ profile,
4225
+ plugins,
4226
+ source,
4227
+ intrinsic,
4228
+ initialDocument,
4229
+ children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ThemeContext.Provider, { value: mergeTheme(theme), children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(UiStateProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(HostCallbacksProvider, { value: { requestImages, onError }, children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(InteractionProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
4230
+ EditorBody,
4231
+ {
4232
+ slots: mergeSlots(slots),
4233
+ onExport,
4234
+ onCancel,
4235
+ onError,
4236
+ exportSpec,
4237
+ stickers
4238
+ }
4239
+ ) }) }) }) })
4240
+ }
4241
+ );
4242
+ }
4243
+
4244
+ // src/gestures/EditorGestures.tsx
4245
+ var import_core32 = require("@pictor-sdk/core");
4246
+ var import_skia11 = require("@pictor-sdk/skia");
4247
+ var import_react23 = require("react");
4248
+ var import_react_native28 = require("react-native");
4249
+ var import_react_native_gesture_handler9 = require("react-native-gesture-handler");
4250
+ var import_react_native_reanimated8 = __toESM(require("react-native-reanimated"));
4251
+ var import_react_native_worklets8 = require("react-native-worklets");
4252
+
4253
+ // src/gestures/controller.ts
4254
+ var import_core31 = require("@pictor-sdk/core");
4255
+ function createGestureController(store, getMetrics, options = {}) {
4256
+ let active = 0;
4257
+ let base = null;
4258
+ let layerId = null;
4259
+ let pan = { x: 0, y: 0 };
4260
+ let pinch = 1;
4261
+ let rotate = 0;
4262
+ const snapState = { rotation: false, centerX: false, centerY: false };
4263
+ function targetLayerId() {
4264
+ return store.getState().selection[0] ?? null;
4265
+ }
4266
+ function begin() {
4267
+ if (active === 0) {
4268
+ layerId = targetLayerId();
4269
+ if (!layerId) return;
4270
+ const layer = store.getState().document.layers.find((l) => l.id === layerId);
4271
+ if (!layer || layer.locked) {
4272
+ layerId = null;
4273
+ return;
4274
+ }
4275
+ base = { ...layer.transform };
4276
+ pan = { x: 0, y: 0 };
4277
+ pinch = 1;
4278
+ rotate = 0;
4279
+ snapState.rotation = snapState.centerX = snapState.centerY = false;
4280
+ store.dispatch({ type: "history/beginBatch", label: "gesture" });
4281
+ }
4282
+ active++;
4283
+ }
4284
+ function fireSnap(kind, snapped) {
4285
+ if (snapped && !snapState[kind]) options.onSnap?.({ kind });
4286
+ snapState[kind] = snapped;
4287
+ }
4288
+ function commit() {
4289
+ if (!base || !layerId) return;
4290
+ const metrics = getMetrics();
4291
+ if (!metrics || metrics.width === 0 || metrics.height === 0) return;
4292
+ const rawX = base.x + pan.x / metrics.width;
4293
+ const rawY = base.y + pan.y / metrics.height;
4294
+ const rawRotation = base.rotation + rotate;
4295
+ const sx = (0, import_core31.snapCenter)(rawX);
4296
+ const sy = (0, import_core31.snapCenter)(rawY);
4297
+ const sr = (0, import_core31.snapRotation)(rawRotation);
4298
+ fireSnap("centerX", sx.snapped);
4299
+ fireSnap("centerY", sy.snapped);
4300
+ fireSnap("rotation", sr.snapped);
4301
+ store.dispatch({
4302
+ type: "layer/transform",
4303
+ id: layerId,
4304
+ patch: {
4305
+ x: sx.value,
4306
+ y: sy.value,
4307
+ rotation: sr.value,
4308
+ scale: Math.max(0.01, base.scale * pinch)
4309
+ }
4310
+ });
4311
+ }
4312
+ function end() {
4313
+ if (active === 0) return;
4314
+ active--;
4315
+ if (active === 0) {
4316
+ if (layerId) store.dispatch({ type: "history/endBatch" });
4317
+ base = null;
4318
+ layerId = null;
4319
+ }
4320
+ }
4321
+ return {
4322
+ /** Pan/pinch/rotate lifecycles — call begin/end once per recognizer. */
4323
+ begin,
4324
+ end,
4325
+ setPan(xDp, yDp) {
4326
+ pan = { x: xDp, y: yDp };
4327
+ commit();
4328
+ },
4329
+ setPinch(scale) {
4330
+ pinch = scale;
4331
+ commit();
4332
+ },
4333
+ setRotate(rad) {
4334
+ rotate = rad;
4335
+ commit();
4336
+ },
4337
+ /** Tap in canvas dp → hit-test → select (empty tap clears selection). */
4338
+ tap(xDp, yDp) {
4339
+ const metrics = getMetrics();
4340
+ if (!metrics) return;
4341
+ const point = { x: xDp / metrics.width, y: yDp / metrics.height };
4342
+ const hit = (0, import_core31.hitTestDocument)(store.getState().document, point, store.registry);
4343
+ store.select(hit ? [hit.id] : []);
4344
+ },
4345
+ /** true while a gesture is in flight (used to mute other UI). */
4346
+ isActive: () => active > 0
4347
+ };
4348
+ }
4349
+
4350
+ // src/gestures/EditorGestures.tsx
4351
+ var import_jsx_runtime31 = require("react/jsx-runtime");
4352
+ function EditorGestures({ onSnap }) {
4353
+ const store = (0, import_core32.useEditorStore)();
4354
+ const metrics = (0, import_skia11.useCanvasMetrics)();
4355
+ const metricsRef = (0, import_react23.useRef)(metrics);
4356
+ metricsRef.current = metrics;
4357
+ const onSnapRef = (0, import_react23.useRef)(onSnap);
4358
+ onSnapRef.current = onSnap;
4359
+ const controller = (0, import_react23.useMemo)(
4360
+ () => createGestureController(store, () => metricsRef.current, {
4361
+ onSnap: (e) => onSnapRef.current?.(e)
4362
+ }),
4363
+ // metricsRef/onSnap are read through refs — store identity is the only dep
4364
+ [store]
4365
+ );
4366
+ const gesture = (0, import_react23.useMemo)(() => {
4367
+ const tap = import_react_native_gesture_handler9.Gesture.Tap().onEnd((e) => {
4368
+ "worklet";
4369
+ (0, import_react_native_worklets8.runOnJS)(controller.tap)(e.x, e.y);
4370
+ });
4371
+ const pan = import_react_native_gesture_handler9.Gesture.Pan().minPointers(1).onStart(() => {
4372
+ "worklet";
4373
+ (0, import_react_native_worklets8.runOnJS)(controller.begin)();
4374
+ }).onUpdate((e) => {
4375
+ "worklet";
4376
+ (0, import_react_native_worklets8.runOnJS)(controller.setPan)(e.translationX, e.translationY);
4377
+ }).onFinalize(() => {
4378
+ "worklet";
4379
+ (0, import_react_native_worklets8.runOnJS)(controller.end)();
4380
+ });
4381
+ const pinch = import_react_native_gesture_handler9.Gesture.Pinch().onStart(() => {
4382
+ "worklet";
4383
+ (0, import_react_native_worklets8.runOnJS)(controller.begin)();
4384
+ }).onUpdate((e) => {
4385
+ "worklet";
4386
+ (0, import_react_native_worklets8.runOnJS)(controller.setPinch)(e.scale);
4387
+ }).onFinalize(() => {
4388
+ "worklet";
4389
+ (0, import_react_native_worklets8.runOnJS)(controller.end)();
4390
+ });
4391
+ const rotate = import_react_native_gesture_handler9.Gesture.Rotation().onStart(() => {
4392
+ "worklet";
4393
+ (0, import_react_native_worklets8.runOnJS)(controller.begin)();
4394
+ }).onUpdate((e) => {
4395
+ "worklet";
4396
+ (0, import_react_native_worklets8.runOnJS)(controller.setRotate)(e.rotation);
4397
+ }).onFinalize(() => {
4398
+ "worklet";
4399
+ (0, import_react_native_worklets8.runOnJS)(controller.end)();
4400
+ });
4401
+ return import_react_native_gesture_handler9.Gesture.Race(
4402
+ import_react_native_gesture_handler9.Gesture.Simultaneous(pan, pinch, rotate),
4403
+ tap
4404
+ );
4405
+ }, [controller]);
4406
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_react_native_gesture_handler9.GestureDetector, { gesture, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_react_native_reanimated8.default.View, { style: import_react_native28.StyleSheet.absoluteFill }) });
4407
+ }
4408
+
4409
+ // src/index.ts
4410
+ var import_core34 = require("@pictor-sdk/core");
4411
+
4412
+ // src/panels/TextPanel.tsx
4413
+ var import_core33 = require("@pictor-sdk/core");
4414
+ var import_react_native29 = require("react-native");
4415
+ var import_jsx_runtime32 = require("react/jsx-runtime");
4416
+ var SWATCHES3 = ["#ffffff", "#000000", "#ed4956", "#ffe25c", "#78de85", "#4dc4ff", "#a15fed"];
4417
+ var ALIGNS2 = ["left", "center", "right"];
4418
+ var FILL_MODES2 = [void 0, "solid", "perLine"];
4419
+ function TextPanel() {
4420
+ const { document, selection, dispatch, select } = (0, import_core33.useEditor)();
4421
+ const store = (0, import_core33.useEditorStore)();
4422
+ const theme = useTheme();
4423
+ const selected = document.layers.find(
4424
+ (l) => l.type === "text" && selection.includes(l.id)
4425
+ );
4426
+ const data = selected ? selected.data : null;
4427
+ const span = data?.spans[0] ?? null;
4428
+ const addText = () => {
4429
+ const id = (0, import_core33.createId)("text");
4430
+ dispatch({
4431
+ type: "layer/add",
4432
+ layer: {
4433
+ id,
4434
+ type: "text",
4435
+ transform: { x: 0.5, y: 0.4, scale: 1, rotation: 0 },
4436
+ opacity: 1,
4437
+ blendMode: "normal",
4438
+ data: {
4439
+ spans: [{ text: "Tap to edit", fontFamily: "System", fontSize: 0.07, color: "#ffffff" }],
4440
+ align: "center",
4441
+ maxWidth: 0.9
4442
+ }
4443
+ }
4444
+ });
4445
+ select([id]);
4446
+ };
4447
+ const patchSpan = (patch) => {
4448
+ if (!selected || !span) return;
4449
+ dispatch({
4450
+ type: "layer/patchData",
4451
+ id: selected.id,
4452
+ patch: { spans: [{ ...span, ...patch }, ...data.spans.slice(1) ?? []] }
4453
+ });
4454
+ };
4455
+ const fonts = ["System", ...store.registry.fonts.map((f) => f.family)];
4456
+ if (!selected || !span || !data) {
4457
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_react_native29.View, { style: { backgroundColor: theme.colors.background, padding: theme.spacing.md, alignItems: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
4458
+ import_react_native29.Pressable,
4459
+ {
4460
+ onPress: addText,
4461
+ style: {
4462
+ paddingHorizontal: theme.spacing.lg,
4463
+ paddingVertical: theme.spacing.sm,
4464
+ borderRadius: theme.radii.md,
4465
+ backgroundColor: theme.colors.surface
4466
+ },
4467
+ children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_react_native29.Text, { style: { color: theme.colors.accent, fontSize: theme.font.size, fontWeight: theme.font.weight }, children: "+ Add text" })
4468
+ }
4469
+ ) });
4470
+ }
4471
+ const cycleAlign = () => {
4472
+ const next = ALIGNS2[(ALIGNS2.indexOf(data.align) + 1) % ALIGNS2.length];
4473
+ dispatch({ type: "layer/patchData", id: selected.id, patch: { align: next } });
4474
+ };
4475
+ const cycleFill = () => {
4476
+ const current = data.fill?.mode;
4477
+ const index = FILL_MODES2.indexOf(current);
4478
+ const next = FILL_MODES2[(index + 1) % FILL_MODES2.length];
4479
+ dispatch({
4480
+ type: "layer/patchData",
4481
+ id: selected.id,
4482
+ patch: {
4483
+ fill: next ? { color: span.color === "#000000" ? "#ffffff" : "#000000", radius: 0.012, padding: 0.012, mode: next } : void 0
4484
+ }
4485
+ });
4486
+ };
4487
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_react_native29.View, { style: { backgroundColor: theme.colors.background }, children: [
4488
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_react_native29.View, { style: { paddingHorizontal: theme.spacing.md }, children: [
4489
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
4490
+ import_react_native29.TextInput,
4491
+ {
4492
+ value: span.text,
4493
+ onChangeText: (text) => patchSpan({ text }),
4494
+ multiline: true,
4495
+ placeholder: "Your text",
4496
+ placeholderTextColor: theme.colors.textMuted,
4497
+ style: {
4498
+ color: theme.colors.text,
4499
+ backgroundColor: theme.colors.surface,
4500
+ borderRadius: theme.radii.sm,
4501
+ padding: theme.spacing.sm,
4502
+ fontSize: theme.font.size
4503
+ }
4504
+ }
4505
+ ),
4506
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
4507
+ Slider,
4508
+ {
4509
+ value: span.fontSize,
4510
+ min: 0.02,
4511
+ max: 0.2,
4512
+ centered: false,
4513
+ onStart: () => dispatch({ type: "history/beginBatch", label: "font size" }),
4514
+ onChange: (fontSize) => patchSpan({ fontSize }),
4515
+ onEnd: () => dispatch({ type: "history/endBatch" })
4516
+ }
4517
+ )
4518
+ ] }),
4519
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
4520
+ import_react_native29.ScrollView,
4521
+ {
4522
+ horizontal: true,
4523
+ showsHorizontalScrollIndicator: false,
4524
+ contentContainerStyle: { gap: theme.spacing.sm, paddingHorizontal: theme.spacing.md, alignItems: "center", paddingBottom: theme.spacing.sm },
4525
+ style: { flexGrow: 0 },
4526
+ children: [
4527
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_react_native29.Pressable, { onPress: cycleAlign, style: { padding: theme.spacing.sm }, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_react_native29.Text, { style: { color: theme.colors.text, fontWeight: theme.font.weight }, children: data.align === "left" ? "\u21E4" : data.align === "center" ? "\u2630" : "\u21E5" }) }),
4528
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_react_native29.Pressable, { onPress: cycleFill, style: { padding: theme.spacing.sm }, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_react_native29.Text, { style: { color: data.fill ? theme.colors.accent : theme.colors.text, fontWeight: theme.font.weight }, children: data.fill?.mode === "perLine" ? "A\u20DE\u20DE" : "A\u20DE" }) }),
4529
+ fonts.map((family) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_react_native29.Pressable, { onPress: () => patchSpan({ fontFamily: family }), style: { padding: theme.spacing.sm }, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
4530
+ import_react_native29.Text,
4531
+ {
4532
+ style: {
4533
+ color: span.fontFamily === family ? theme.colors.accent : theme.colors.textMuted,
4534
+ fontSize: theme.font.sizeSm
4535
+ },
4536
+ children: family
4537
+ }
4538
+ ) }, family)),
4539
+ SWATCHES3.map((color) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
4540
+ import_react_native29.Pressable,
4541
+ {
4542
+ onPress: () => patchSpan({ color }),
4543
+ style: {
4544
+ width: 24,
4545
+ height: 24,
4546
+ borderRadius: 12,
4547
+ backgroundColor: color,
4548
+ borderWidth: span.color === color ? 3 : 1,
4549
+ borderColor: span.color === color ? theme.colors.accent : theme.colors.track
4550
+ }
4551
+ },
4552
+ color
4553
+ ))
4554
+ ]
4555
+ }
4556
+ )
4557
+ ] });
4558
+ }
4559
+ // Annotate the CommonJS export names for ESM import in node:
4560
+ 0 && (module.exports = {
4561
+ AddPhotoPanel,
4562
+ AdjustPanel,
4563
+ CanvasStage,
4564
+ CropGestureSurface,
4565
+ CropGrid,
4566
+ CropPanel,
4567
+ DEFAULT_BRUSH,
4568
+ DrawPanel,
4569
+ DrawingSurface,
4570
+ Editor,
4571
+ EditorGestures,
4572
+ FilterStrip,
4573
+ HostCallbacksProvider,
4574
+ InteractionGuides,
4575
+ InteractionProvider,
4576
+ InvalidImageResultError,
4577
+ LayerGestureSurface,
4578
+ LayersPanel,
4579
+ LayoutPanel,
4580
+ MediaPanel,
4581
+ POST_PROFILE,
4582
+ STORY_PROFILE,
4583
+ STRAIGHTEN_LIMIT,
4584
+ Slider,
4585
+ StickerPanel,
4586
+ StoryMediaGestureSurface,
4587
+ StraightenRuler,
4588
+ TextComposer,
4589
+ TextComposerHost,
4590
+ TextPanel,
4591
+ ThemeContext,
4592
+ ToolPanelHost,
4593
+ ToolRail,
4594
+ TopBar,
4595
+ UiStateProvider,
4596
+ canvasSizeForAspect,
4597
+ cellToTransformPatch,
4598
+ centeredCropForAspect,
4599
+ createAddPhotoController,
4600
+ createDrawController,
4601
+ createGestureController,
4602
+ createUiStateStore,
4603
+ defaultTheme,
4604
+ layoutCells,
4605
+ mergeSlots,
4606
+ mergeTheme,
4607
+ radiansToStraighten,
4608
+ resolveGestureSurface,
4609
+ resolvePanel,
4610
+ straightenToRadians,
4611
+ useBaseImageLayer,
4612
+ useBaseMediaLayer,
4613
+ useHostCallbacks,
4614
+ useInteractionState,
4615
+ useTargetImageLayer,
4616
+ useTextSession,
4617
+ useTheme,
4618
+ useUiState
4619
+ });
4620
+ //# sourceMappingURL=index.js.map