canvu-react 0.4.52 → 0.4.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/native.cjs CHANGED
@@ -1199,6 +1199,9 @@ function cloneVectorSceneItemWithNewId(item) {
1199
1199
  }
1200
1200
  return next;
1201
1201
  }
1202
+ function cloneVectorSceneItemsWithNewIds(items) {
1203
+ return items.map(cloneVectorSceneItemWithNewId);
1204
+ }
1202
1205
 
1203
1206
  // src/scene/managed-images.ts
1204
1207
  var MANAGED_KEY = "managed";
@@ -1262,8 +1265,68 @@ function rotateManagedImage(items, id) {
1262
1265
  function deleteManagedImage(items, id) {
1263
1266
  return restackManagedImages(items.filter((i) => i.id !== id));
1264
1267
  }
1268
+ function reorderManagedImages(items, orderedManagedIds) {
1269
+ const managedSlots = [];
1270
+ for (let i = 0; i < items.length; i++) {
1271
+ const item = items[i];
1272
+ if (item && isManagedImage(item)) managedSlots.push(i);
1273
+ }
1274
+ if (managedSlots.length !== orderedManagedIds.length) {
1275
+ return [...items];
1276
+ }
1277
+ const byId = new Map(items.map((i) => [i.id, i]));
1278
+ const next = [...items];
1279
+ managedSlots.forEach((slot, k) => {
1280
+ const orderedId = orderedManagedIds[k];
1281
+ if (orderedId === void 0) return;
1282
+ const replacement = byId.get(orderedId);
1283
+ if (replacement) next[slot] = replacement;
1284
+ });
1285
+ return restackManagedImages(next);
1286
+ }
1287
+
1288
+ // src/native/native-images-menu-reorder.ts
1289
+ function moveNativeManagedImageId(managedIds, fromIndex, toIndex) {
1290
+ const next = [...managedIds];
1291
+ if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= next.length || toIndex >= next.length) {
1292
+ return next;
1293
+ }
1294
+ const [item] = next.splice(fromIndex, 1);
1295
+ if (item === void 0) return next;
1296
+ next.splice(toIndex, 0, item);
1297
+ return next;
1298
+ }
1299
+ function getNativeImagesMenuReorderIndex({
1300
+ activeId,
1301
+ managedIds,
1302
+ rowLayouts,
1303
+ dragDeltaY
1304
+ }) {
1305
+ const currentIndex = managedIds.indexOf(activeId);
1306
+ if (currentIndex < 0) return -1;
1307
+ const layoutById = new Map(rowLayouts.map((layout) => [layout.id, layout]));
1308
+ const activeLayout = layoutById.get(activeId);
1309
+ if (!activeLayout) return currentIndex;
1310
+ const activeCenterY = activeLayout.y + activeLayout.height / 2 + dragDeltaY;
1311
+ let nextIndex = currentIndex;
1312
+ let closestDistance = Infinity;
1313
+ for (let index = 0; index < managedIds.length; index++) {
1314
+ const id = managedIds[index];
1315
+ if (id === void 0) continue;
1316
+ const layout = layoutById.get(id);
1317
+ if (!layout) continue;
1318
+ const centerY = layout.y + layout.height / 2;
1319
+ const distance = Math.abs(activeCenterY - centerY);
1320
+ if (distance < closestDistance) {
1321
+ closestDistance = distance;
1322
+ nextIndex = index;
1323
+ }
1324
+ }
1325
+ return nextIndex;
1326
+ }
1265
1327
  var defaultLabels = {
1266
1328
  title: "Images",
1329
+ dragHandle: "Drag to reorder",
1267
1330
  focus: "Focus on canvas",
1268
1331
  duplicate: "Duplicate",
1269
1332
  rotate: "Rotate",
@@ -1335,61 +1398,110 @@ function NativeImagesMenuRow({
1335
1398
  item,
1336
1399
  items,
1337
1400
  labels,
1401
+ isDragging,
1402
+ isDropTarget,
1403
+ dragDeltaY,
1338
1404
  onItemsChange,
1339
1405
  onFocusItem,
1406
+ onDragStart,
1407
+ onDragMove,
1408
+ onDragEnd,
1409
+ onRowLayout,
1340
1410
  getImageUri,
1341
1411
  renderThumbnail,
1342
1412
  rowStyle,
1343
1413
  actionButtonStyle
1344
1414
  }) {
1345
1415
  const imageUri = getImageUri(item);
1346
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.row, rowStyle], children: [
1347
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.handle, children: /* @__PURE__ */ jsxRuntime.jsx(NativeImagesMenuIcon, { name: "grip", color: "#94a3b8" }) }),
1348
- /* @__PURE__ */ jsxRuntime.jsx(
1349
- reactNative.Pressable,
1350
- {
1351
- accessibilityRole: "button",
1352
- accessibilityLabel: labels.focus,
1353
- disabled: !onFocusItem,
1354
- onPress: () => onFocusItem?.(item),
1355
- style: styles.thumbnailButton,
1356
- children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.thumbnailBox, children: [
1357
- renderThumbnail ? renderThumbnail(item) : null,
1358
- !renderThumbnail && imageUri ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Image, { source: { uri: imageUri }, style: styles.thumbnailImage }) : null
1416
+ const panResponder = react.useMemo(
1417
+ () => reactNative.PanResponder.create({
1418
+ onStartShouldSetPanResponder: () => true,
1419
+ onMoveShouldSetPanResponder: () => true,
1420
+ onPanResponderGrant: () => onDragStart(item.id),
1421
+ onPanResponderMove: (_event, gestureState) => onDragMove(item.id, gestureState.dy),
1422
+ onPanResponderRelease: (_event, gestureState) => onDragEnd(item.id, gestureState.dy),
1423
+ onPanResponderTerminate: (_event, gestureState) => onDragEnd(item.id, gestureState.dy),
1424
+ onPanResponderTerminationRequest: () => false,
1425
+ onShouldBlockNativeResponder: () => true
1426
+ }),
1427
+ [item.id, onDragEnd, onDragMove, onDragStart]
1428
+ );
1429
+ const onLayout = react.useCallback(
1430
+ (event) => {
1431
+ const { height, y } = event.nativeEvent.layout;
1432
+ onRowLayout({ id: item.id, y, height });
1433
+ },
1434
+ [item.id, onRowLayout]
1435
+ );
1436
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1437
+ reactNative.View,
1438
+ {
1439
+ onLayout,
1440
+ style: [
1441
+ styles.row,
1442
+ rowStyle,
1443
+ isDropTarget && styles.dropTargetRow,
1444
+ isDragging && styles.draggingRow,
1445
+ isDragging && { transform: [{ translateY: dragDeltaY }] }
1446
+ ],
1447
+ children: [
1448
+ /* @__PURE__ */ jsxRuntime.jsx(
1449
+ reactNative.View,
1450
+ {
1451
+ accessibilityRole: "button",
1452
+ accessibilityLabel: labels.dragHandle,
1453
+ style: [styles.handle, isDragging && styles.handleDragging],
1454
+ ...panResponder.panHandlers,
1455
+ children: /* @__PURE__ */ jsxRuntime.jsx(NativeImagesMenuIcon, { name: "grip", color: "#94a3b8" })
1456
+ }
1457
+ ),
1458
+ /* @__PURE__ */ jsxRuntime.jsx(
1459
+ reactNative.Pressable,
1460
+ {
1461
+ accessibilityRole: "button",
1462
+ accessibilityLabel: labels.focus,
1463
+ disabled: !onFocusItem,
1464
+ onPress: () => onFocusItem?.(item),
1465
+ style: styles.thumbnailButton,
1466
+ children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.thumbnailBox, children: [
1467
+ renderThumbnail ? renderThumbnail(item) : null,
1468
+ !renderThumbnail && imageUri ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Image, { source: { uri: imageUri }, style: styles.thumbnailImage }) : null
1469
+ ] })
1470
+ }
1471
+ ),
1472
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.actionsColumn, children: [
1473
+ /* @__PURE__ */ jsxRuntime.jsx(
1474
+ NativeImagesMenuAction,
1475
+ {
1476
+ label: labels.duplicate,
1477
+ onPress: () => onItemsChange(copyManagedImage(items, item.id)),
1478
+ style: actionButtonStyle,
1479
+ children: /* @__PURE__ */ jsxRuntime.jsx(NativeImagesMenuIcon, { name: "copy" })
1480
+ }
1481
+ ),
1482
+ /* @__PURE__ */ jsxRuntime.jsx(
1483
+ NativeImagesMenuAction,
1484
+ {
1485
+ label: labels.rotate,
1486
+ onPress: () => onItemsChange(rotateManagedImage(items, item.id)),
1487
+ style: actionButtonStyle,
1488
+ children: /* @__PURE__ */ jsxRuntime.jsx(NativeImagesMenuIcon, { name: "rotate" })
1489
+ }
1490
+ ),
1491
+ /* @__PURE__ */ jsxRuntime.jsx(
1492
+ NativeImagesMenuAction,
1493
+ {
1494
+ label: labels.delete,
1495
+ danger: true,
1496
+ onPress: () => onItemsChange(deleteManagedImage(items, item.id)),
1497
+ style: actionButtonStyle,
1498
+ children: /* @__PURE__ */ jsxRuntime.jsx(NativeImagesMenuIcon, { name: "trash", color: "#b91c1c" })
1499
+ }
1500
+ )
1359
1501
  ] })
1360
- }
1361
- ),
1362
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.actionsColumn, children: [
1363
- /* @__PURE__ */ jsxRuntime.jsx(
1364
- NativeImagesMenuAction,
1365
- {
1366
- label: labels.duplicate,
1367
- onPress: () => onItemsChange(copyManagedImage(items, item.id)),
1368
- style: actionButtonStyle,
1369
- children: /* @__PURE__ */ jsxRuntime.jsx(NativeImagesMenuIcon, { name: "copy" })
1370
- }
1371
- ),
1372
- /* @__PURE__ */ jsxRuntime.jsx(
1373
- NativeImagesMenuAction,
1374
- {
1375
- label: labels.rotate,
1376
- onPress: () => onItemsChange(rotateManagedImage(items, item.id)),
1377
- style: actionButtonStyle,
1378
- children: /* @__PURE__ */ jsxRuntime.jsx(NativeImagesMenuIcon, { name: "rotate" })
1379
- }
1380
- ),
1381
- /* @__PURE__ */ jsxRuntime.jsx(
1382
- NativeImagesMenuAction,
1383
- {
1384
- label: labels.delete,
1385
- danger: true,
1386
- onPress: () => onItemsChange(deleteManagedImage(items, item.id)),
1387
- style: actionButtonStyle,
1388
- children: /* @__PURE__ */ jsxRuntime.jsx(NativeImagesMenuIcon, { name: "trash", color: "#b91c1c" })
1389
- }
1390
- )
1391
- ] })
1392
- ] });
1502
+ ]
1503
+ }
1504
+ );
1393
1505
  }
1394
1506
  function NativeImagesMenu({
1395
1507
  items,
@@ -1406,7 +1518,69 @@ function NativeImagesMenu({
1406
1518
  renderThumbnail
1407
1519
  }) {
1408
1520
  const managed = react.useMemo(() => items.filter(isManagedImage), [items]);
1521
+ const managedIds = react.useMemo(() => managed.map((item) => item.id), [managed]);
1409
1522
  const [collapsed, setCollapsed] = react.useState(!defaultOpen);
1523
+ const [dragState, setDragState] = react.useState(null);
1524
+ const rowLayoutsRef = react.useRef(/* @__PURE__ */ new Map());
1525
+ const getRowLayouts = react.useCallback(
1526
+ () => managedIds.map((id) => rowLayoutsRef.current.get(id)).filter((layout) => layout != null),
1527
+ [managedIds]
1528
+ );
1529
+ const handleRowLayout = react.useCallback((layout) => {
1530
+ rowLayoutsRef.current.set(layout.id, layout);
1531
+ }, []);
1532
+ const handleDragStart = react.useCallback(
1533
+ (id) => {
1534
+ const fromIndex = managedIds.indexOf(id);
1535
+ if (fromIndex < 0) return;
1536
+ setDragState({
1537
+ activeId: id,
1538
+ fromIndex,
1539
+ currentIndex: fromIndex,
1540
+ dragDeltaY: 0
1541
+ });
1542
+ },
1543
+ [managedIds]
1544
+ );
1545
+ const handleDragMove = react.useCallback(
1546
+ (id, dragDeltaY) => {
1547
+ setDragState((current) => {
1548
+ if (!current || current.activeId !== id) return current;
1549
+ const currentIndex = getNativeImagesMenuReorderIndex({
1550
+ activeId: id,
1551
+ managedIds,
1552
+ rowLayouts: getRowLayouts(),
1553
+ dragDeltaY
1554
+ });
1555
+ return {
1556
+ ...current,
1557
+ currentIndex: currentIndex >= 0 ? currentIndex : current.fromIndex,
1558
+ dragDeltaY
1559
+ };
1560
+ });
1561
+ },
1562
+ [getRowLayouts, managedIds]
1563
+ );
1564
+ const handleDragEnd = react.useCallback(
1565
+ (id, dragDeltaY) => {
1566
+ const fromIndex = managedIds.indexOf(id);
1567
+ const currentIndex = getNativeImagesMenuReorderIndex({
1568
+ activeId: id,
1569
+ managedIds,
1570
+ rowLayouts: getRowLayouts(),
1571
+ dragDeltaY
1572
+ });
1573
+ setDragState(null);
1574
+ if (fromIndex < 0 || currentIndex < 0 || fromIndex === currentIndex) return;
1575
+ const orderedIds = moveNativeManagedImageId(
1576
+ managedIds,
1577
+ fromIndex,
1578
+ currentIndex
1579
+ );
1580
+ onItemsChange(reorderManagedImages(items, orderedIds));
1581
+ },
1582
+ [getRowLayouts, items, managedIds, onItemsChange]
1583
+ );
1410
1584
  if (managed.length === 0) return null;
1411
1585
  const resolvedLabels = resolveLabels(labels);
1412
1586
  if (collapsed) {
@@ -1451,15 +1625,23 @@ function NativeImagesMenu({
1451
1625
  {
1452
1626
  style: styles.listScroll,
1453
1627
  contentContainerStyle: styles.listContent,
1628
+ scrollEnabled: dragState == null,
1454
1629
  showsVerticalScrollIndicator: false,
1455
- children: managed.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
1630
+ children: managed.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
1456
1631
  NativeImagesMenuRow,
1457
1632
  {
1458
1633
  item,
1459
1634
  items,
1460
1635
  labels: resolvedLabels,
1636
+ isDragging: dragState?.activeId === item.id,
1637
+ isDropTarget: dragState != null && dragState.activeId !== item.id && dragState.currentIndex === index,
1638
+ dragDeltaY: dragState?.activeId === item.id ? dragState.dragDeltaY : 0,
1461
1639
  onItemsChange,
1462
1640
  onFocusItem,
1641
+ onDragStart: handleDragStart,
1642
+ onDragMove: handleDragMove,
1643
+ onDragEnd: handleDragEnd,
1644
+ onRowLayout: handleRowLayout,
1463
1645
  getImageUri,
1464
1646
  renderThumbnail,
1465
1647
  rowStyle,
@@ -1519,6 +1701,19 @@ var styles = reactNative.StyleSheet.create({
1519
1701
  dangerActionButton: {
1520
1702
  backgroundColor: "#fef2f2"
1521
1703
  },
1704
+ draggingRow: {
1705
+ backgroundColor: "#eef2f7",
1706
+ elevation: 4,
1707
+ opacity: 0.85,
1708
+ zIndex: 1
1709
+ },
1710
+ dropTargetRow: {
1711
+ backgroundColor: "#f8fafc"
1712
+ },
1713
+ handleDragging: {
1714
+ backgroundColor: "#e2e8f0",
1715
+ borderRadius: 6
1716
+ },
1522
1717
  handle: {
1523
1718
  alignItems: "center",
1524
1719
  height: 128,
@@ -3078,22 +3273,6 @@ function resolveNativeStrokePreviewStyle(tool, previewStrokeStyle) {
3078
3273
  };
3079
3274
  }
3080
3275
 
3081
- // src/native/native-stroke-preview.ts
3082
- function buildNativeStrokePreviewPath(points) {
3083
- if (points.length < 2) return null;
3084
- const d = smoothFreehandPointsToPathD(points);
3085
- return d || null;
3086
- }
3087
- function buildNativeFreehandStrokePreviewPayload(points, style, tool) {
3088
- if (tool === "laser") return null;
3089
- return computeFreehandSvgPayload(
3090
- points.map((point) => ({ x: point.x, y: point.y })),
3091
- style,
3092
- tool,
3093
- true
3094
- );
3095
- }
3096
-
3097
3276
  // src/native/native-vector-interactions.ts
3098
3277
  var NATIVE_SELECTION_HANDLE_HIT_RADIUS_PX = 24;
3099
3278
  function nativeItemPlacementBounds(item) {
@@ -3161,6 +3340,26 @@ function pointInNativeSelectedItemBounds(item, worldPoint) {
3161
3340
  );
3162
3341
  return local.x >= 0 && local.x <= bounds.width && local.y >= 0 && local.y <= bounds.height;
3163
3342
  }
3343
+ function resolveNativeSelectionContextMenuRequest({
3344
+ interactive,
3345
+ toolId,
3346
+ selectedItems,
3347
+ worldPoint,
3348
+ screenPoint
3349
+ }) {
3350
+ if (!interactive || toolId !== "select") return null;
3351
+ const editableSelectedItems = selectedItems.filter((item) => !item.locked);
3352
+ if (editableSelectedItems.length === 0) return null;
3353
+ const pressedSelectedItem = editableSelectedItems.some(
3354
+ (item) => pointInNativeSelectedItemBounds(item, worldPoint)
3355
+ );
3356
+ if (!pressedSelectedItem) return null;
3357
+ return {
3358
+ itemIds: editableSelectedItems.map((item) => item.id),
3359
+ x: screenPoint.x,
3360
+ y: screenPoint.y
3361
+ };
3362
+ }
3164
3363
  function resolveNativeCustomPlacement(toolId, customPlacement, customPlacements) {
3165
3364
  if (customPlacement?.toolId === toolId) return customPlacement;
3166
3365
  return customPlacements?.find((placement) => placement.toolId === toolId) ?? null;
@@ -3465,90 +3664,58 @@ function NativeInteractionOverlay({
3465
3664
  p.tool,
3466
3665
  p.style ?? previewStrokeStyle
3467
3666
  );
3468
- const strokeColor = colorWithOpacity(
3469
- style.stroke,
3470
- isLaser ? 0.85 : style.strokeOpacity
3667
+ const payload = computeFreehandSvgPayload(
3668
+ p.points,
3669
+ style,
3670
+ isLaser ? "draw" : p.tool,
3671
+ p.points.length === 2
3471
3672
  );
3472
- if (!isLaser) {
3473
- const payload = buildNativeFreehandStrokePreviewPayload(
3474
- p.points,
3475
- style,
3476
- p.tool
3673
+ if (!payload) return null;
3674
+ if (payload.kind === "circle") {
3675
+ return /* @__PURE__ */ jsxRuntime.jsx(
3676
+ reactNativeSkia.Circle,
3677
+ {
3678
+ cx: payload.cx,
3679
+ cy: payload.cy,
3680
+ r: payload.r,
3681
+ color: colorWithOpacity(payload.fill, payload.fillOpacity),
3682
+ style: "fill",
3683
+ antiAlias: true
3684
+ }
3477
3685
  );
3478
- if (payload?.kind === "circle") {
3479
- return /* @__PURE__ */ jsxRuntime.jsx(
3480
- reactNativeSkia.Circle,
3481
- {
3482
- cx: payload.cx,
3483
- cy: payload.cy,
3484
- r: payload.r,
3485
- color: colorWithOpacity(payload.fill, payload.fillOpacity),
3486
- style: "fill",
3487
- antiAlias: true
3488
- }
3489
- );
3490
- }
3491
- if (payload?.kind === "fillPath") {
3492
- return /* @__PURE__ */ jsxRuntime.jsx(
3493
- reactNativeSkia.Path,
3494
- {
3495
- path: payload.d,
3496
- color: colorWithOpacity(payload.fill, payload.fillOpacity),
3497
- style: "fill",
3498
- fillType: "winding",
3499
- antiAlias: true
3500
- }
3501
- );
3502
- }
3503
- if (payload?.kind === "strokePath") {
3504
- const intervals = dashIntervalsFromStrokeDasharray3(
3505
- payload.strokeDasharray
3506
- );
3507
- return /* @__PURE__ */ jsxRuntime.jsx(
3508
- reactNativeSkia.Path,
3509
- {
3510
- path: payload.d,
3511
- color: colorWithOpacity(payload.stroke, payload.strokeOpacity),
3512
- style: "stroke",
3513
- strokeWidth: payload.strokeWidth,
3514
- strokeCap: "round",
3515
- strokeJoin: "round",
3516
- antiAlias: true,
3517
- children: intervals ? /* @__PURE__ */ jsxRuntime.jsx(reactNativeSkia.DashPathEffect, { intervals }) : null
3518
- }
3519
- );
3520
- }
3521
- return null;
3522
3686
  }
3523
- if (p.points.length === 1) {
3524
- const point = p.points[0];
3525
- if (!point) return null;
3687
+ if (payload.kind === "fillPath") {
3526
3688
  return /* @__PURE__ */ jsxRuntime.jsx(
3527
- reactNativeSkia.Circle,
3689
+ reactNativeSkia.Path,
3528
3690
  {
3529
- cx: point.x,
3530
- cy: point.y,
3531
- r: Math.max(0.5, style.strokeWidth / 2),
3532
- color: strokeColor,
3691
+ path: payload.d,
3692
+ color: colorWithOpacity(payload.fill, payload.fillOpacity),
3533
3693
  style: "fill",
3694
+ fillType: "winding",
3534
3695
  antiAlias: true
3535
3696
  }
3536
3697
  );
3537
3698
  }
3538
- const path = buildNativeStrokePreviewPath(p.points);
3539
- if (!path) return null;
3540
- return /* @__PURE__ */ jsxRuntime.jsx(
3541
- reactNativeSkia.Path,
3542
- {
3543
- path,
3544
- color: strokeColor,
3545
- style: "stroke",
3546
- strokeWidth: style.strokeWidth,
3547
- strokeCap: "round",
3548
- strokeJoin: "round",
3549
- antiAlias: true
3550
- }
3551
- );
3699
+ if (payload.kind === "strokePath") {
3700
+ const intervals = dashIntervalsFromStrokeDasharray3(payload.strokeDasharray);
3701
+ return /* @__PURE__ */ jsxRuntime.jsx(
3702
+ reactNativeSkia.Path,
3703
+ {
3704
+ path: payload.d,
3705
+ color: colorWithOpacity(
3706
+ payload.stroke,
3707
+ isLaser ? 0.85 : payload.strokeOpacity
3708
+ ),
3709
+ style: "stroke",
3710
+ strokeWidth: payload.strokeWidth,
3711
+ strokeCap: "round",
3712
+ strokeJoin: "round",
3713
+ antiAlias: true,
3714
+ children: intervals ? /* @__PURE__ */ jsxRuntime.jsx(reactNativeSkia.DashPathEffect, { intervals }) : null
3715
+ }
3716
+ );
3717
+ }
3718
+ return null;
3552
3719
  }
3553
3720
  return null;
3554
3721
  }, [placementPreview, previewStrokeStyle, overlayStrokeWorld, marqueeDashWorld]);
@@ -5112,6 +5279,18 @@ function applyRotationFromPointer(item, startRotation, startPointerAngleRad, poi
5112
5279
  }
5113
5280
  return { ...item, rotation: startRotation + delta };
5114
5281
  }
5282
+ function moveItemByDelta(item, dx, dy) {
5283
+ return {
5284
+ ...item,
5285
+ x: item.x + dx,
5286
+ y: item.y + dy,
5287
+ bounds: {
5288
+ ...item.bounds,
5289
+ x: item.bounds.x + dx,
5290
+ y: item.bounds.y + dy
5291
+ }
5292
+ };
5293
+ }
5115
5294
  function resizeItemByHandle(item, start, handle, currentWorld) {
5116
5295
  const sb = normalizeRect(start.bounds);
5117
5296
  if (!itemAllowsResizeHandle(item, handle)) {
@@ -5336,6 +5515,39 @@ function hitTestNativeRemotePresence(peers, camera, point) {
5336
5515
  return null;
5337
5516
  }
5338
5517
 
5518
+ // src/native/native-shape-clipboard.ts
5519
+ var NATIVE_SHAPE_CLIPBOARD_PASTE_OFFSET_WORLD = 24;
5520
+ function copyNativeSelectedShapeItems(items, selectedIds) {
5521
+ if (selectedIds.length === 0) return null;
5522
+ const copies = selectedIds.map((id) => items.find((item) => item.id === id)).filter((item) => item != null);
5523
+ if (copies.length === 0) return null;
5524
+ return copies.map((item) => JSON.parse(JSON.stringify(item)));
5525
+ }
5526
+ function pasteNativeShapeClipboard(input) {
5527
+ if (input.clipboard.length === 0) return null;
5528
+ const clones = cloneVectorSceneItemsWithNewIds(input.clipboard);
5529
+ const moved = clones.map(
5530
+ (item) => moveItemByDelta(
5531
+ item,
5532
+ NATIVE_SHAPE_CLIPBOARD_PASTE_OFFSET_WORLD,
5533
+ NATIVE_SHAPE_CLIPBOARD_PASTE_OFFSET_WORLD
5534
+ )
5535
+ );
5536
+ if (moved.length === 0) return null;
5537
+ return {
5538
+ items: [...input.items, ...moved],
5539
+ selectedIds: moved.map((item) => item.id)
5540
+ };
5541
+ }
5542
+ function duplicateNativeSelectedShapes(input) {
5543
+ const clipboard = copyNativeSelectedShapeItems(input.items, input.selectedIds);
5544
+ if (!clipboard) return null;
5545
+ return pasteNativeShapeClipboard({
5546
+ items: input.items,
5547
+ clipboard
5548
+ });
5549
+ }
5550
+
5339
5551
  // src/native/native-transient-items.ts
5340
5552
  function moveNativeTransientItems({
5341
5553
  items,
@@ -5384,6 +5596,11 @@ var NATIVE_VIEWPORT_OVERLAY_Z_INDEX = 40;
5384
5596
  var NATIVE_VIEWPORT_OVERLAY_ELEVATION = 40;
5385
5597
  var NATIVE_VIEWPORT_EXTERNAL_OVERLAY_Z_INDEX = 20;
5386
5598
  var NATIVE_VIEWPORT_EXTERNAL_OVERLAY_ELEVATION = 20;
5599
+ var LONG_PRESS_CONTEXT_MENU_MS = 520;
5600
+ var LONG_PRESS_CONTEXT_MENU_CANCEL_PX = 10;
5601
+ var NATIVE_CONTEXT_MENU_WIDTH = 304;
5602
+ var NATIVE_CONTEXT_MENU_HEIGHT = 52;
5603
+ var NATIVE_CONTEXT_MENU_MARGIN = 12;
5387
5604
  function isPlacementTool(toolId) {
5388
5605
  return toolId === "rect" || toolId === "ellipse" || toolId === "architectural-cloud" || toolId === "line" || toolId === "arrow";
5389
5606
  }
@@ -5456,6 +5673,102 @@ function fitCameraToWorldRect(camera, viewportW, viewportH, worldRect, padding)
5456
5673
  camera.x = viewportW / 2 - cx * z;
5457
5674
  camera.y = viewportH / 2 - cy * z;
5458
5675
  }
5676
+ function clampNativeContextMenuState(menu, size) {
5677
+ return {
5678
+ ...menu,
5679
+ x: Math.max(
5680
+ NATIVE_CONTEXT_MENU_MARGIN,
5681
+ Math.min(
5682
+ menu.x,
5683
+ Math.max(NATIVE_CONTEXT_MENU_MARGIN, size.width - NATIVE_CONTEXT_MENU_WIDTH)
5684
+ )
5685
+ ),
5686
+ y: Math.max(
5687
+ NATIVE_CONTEXT_MENU_MARGIN,
5688
+ Math.min(
5689
+ menu.y - NATIVE_CONTEXT_MENU_HEIGHT - NATIVE_CONTEXT_MENU_MARGIN,
5690
+ Math.max(
5691
+ NATIVE_CONTEXT_MENU_MARGIN,
5692
+ size.height - NATIVE_CONTEXT_MENU_HEIGHT - NATIVE_CONTEXT_MENU_MARGIN
5693
+ )
5694
+ )
5695
+ )
5696
+ };
5697
+ }
5698
+ function NativeSelectionContextMenu({
5699
+ x,
5700
+ y,
5701
+ canPaste,
5702
+ onCopy,
5703
+ onPaste,
5704
+ onDuplicate,
5705
+ onDelete
5706
+ }) {
5707
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5708
+ reactNative.View,
5709
+ {
5710
+ style: [
5711
+ styles4.nativeSelectionContextMenu,
5712
+ {
5713
+ left: x,
5714
+ top: y
5715
+ }
5716
+ ],
5717
+ children: [
5718
+ /* @__PURE__ */ jsxRuntime.jsx(NativeSelectionContextMenuButton, { label: "Copy", onPress: onCopy }),
5719
+ /* @__PURE__ */ jsxRuntime.jsx(
5720
+ NativeSelectionContextMenuButton,
5721
+ {
5722
+ label: "Paste",
5723
+ onPress: onPaste,
5724
+ disabled: !canPaste
5725
+ }
5726
+ ),
5727
+ /* @__PURE__ */ jsxRuntime.jsx(NativeSelectionContextMenuButton, { label: "Duplicate", onPress: onDuplicate }),
5728
+ /* @__PURE__ */ jsxRuntime.jsx(
5729
+ NativeSelectionContextMenuButton,
5730
+ {
5731
+ label: "Delete",
5732
+ onPress: onDelete,
5733
+ destructive: true
5734
+ }
5735
+ )
5736
+ ]
5737
+ }
5738
+ );
5739
+ }
5740
+ function NativeSelectionContextMenuButton({
5741
+ label,
5742
+ onPress,
5743
+ disabled = false,
5744
+ destructive = false
5745
+ }) {
5746
+ return /* @__PURE__ */ jsxRuntime.jsx(
5747
+ reactNative.Pressable,
5748
+ {
5749
+ accessibilityRole: "button",
5750
+ accessibilityState: { disabled },
5751
+ disabled,
5752
+ onPress,
5753
+ style: ({ pressed }) => [
5754
+ styles4.nativeSelectionContextMenuButton,
5755
+ pressed && !disabled ? styles4.nativeSelectionContextMenuButtonPressed : void 0,
5756
+ disabled ? styles4.nativeSelectionContextMenuButtonDisabled : void 0
5757
+ ],
5758
+ children: /* @__PURE__ */ jsxRuntime.jsx(
5759
+ reactNative.Text,
5760
+ {
5761
+ style: [
5762
+ styles4.nativeSelectionContextMenuButtonText,
5763
+ destructive ? styles4.nativeSelectionContextMenuDeleteText : void 0,
5764
+ disabled ? styles4.nativeSelectionContextMenuButtonTextDisabled : void 0
5765
+ ],
5766
+ children: label
5767
+ }
5768
+ )
5769
+ }
5770
+ );
5771
+ }
5459
5772
  var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5460
5773
  items,
5461
5774
  selectedIds = [],
@@ -5522,6 +5835,16 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5522
5835
  selectedIdsRef.current = selectedIds;
5523
5836
  const remotePresenceRef = react.useRef(remotePresence);
5524
5837
  remotePresenceRef.current = remotePresence;
5838
+ const shapeClipboardRef = react.useRef(null);
5839
+ const [selectionContextMenu, setSelectionContextMenu] = react.useState(null);
5840
+ const selectionContextMenuRef = react.useRef(
5841
+ null
5842
+ );
5843
+ selectionContextMenuRef.current = selectionContextMenu;
5844
+ const contextMenuLongPressTimerRef = react.useRef(
5845
+ null
5846
+ );
5847
+ const contextMenuLongPressStartRef = react.useRef(null);
5525
5848
  const dragStateRef = react.useRef({ kind: "idle" });
5526
5849
  react.useEffect(() => {
5527
5850
  const committedItems = items;
@@ -5539,6 +5862,16 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5539
5862
  transientItemsRef.current = null;
5540
5863
  setTransientItems(null);
5541
5864
  }, []);
5865
+ const clearNativeContextMenuLongPress = react.useCallback(() => {
5866
+ if (contextMenuLongPressTimerRef.current) {
5867
+ clearTimeout(contextMenuLongPressTimerRef.current);
5868
+ contextMenuLongPressTimerRef.current = null;
5869
+ }
5870
+ contextMenuLongPressStartRef.current = null;
5871
+ }, []);
5872
+ const closeNativeSelectionContextMenu = react.useCallback(() => {
5873
+ setSelectionContextMenu(null);
5874
+ }, []);
5542
5875
  const commitTransientItemsPreview = react.useCallback(() => {
5543
5876
  const nextItems = transientItemsRef.current;
5544
5877
  const change = onItemsChangeRef.current;
@@ -5571,6 +5904,9 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5571
5904
  if (laserClearTimerRef.current) {
5572
5905
  clearTimeout(laserClearTimerRef.current);
5573
5906
  }
5907
+ if (contextMenuLongPressTimerRef.current) {
5908
+ clearTimeout(contextMenuLongPressTimerRef.current);
5909
+ }
5574
5910
  },
5575
5911
  []
5576
5912
  );
@@ -5655,6 +5991,10 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5655
5991
  }
5656
5992
  const camera = cameraRef.current;
5657
5993
  const [cameraTick, setCameraTick] = react.useState(0);
5994
+ const selectedItems = react.useMemo(
5995
+ () => activeItems.filter((item) => selectedIds.includes(item.id)),
5996
+ [activeItems, selectedIds]
5997
+ );
5658
5998
  const screenToWorld = react.useCallback(
5659
5999
  (sx, sy) => {
5660
6000
  const cam = cameraRef.current;
@@ -5663,6 +6003,59 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5663
6003
  },
5664
6004
  []
5665
6005
  );
6006
+ const startNativeContextMenuLongPress = react.useCallback(
6007
+ (point) => {
6008
+ clearNativeContextMenuLongPress();
6009
+ if (toolIdRef.current !== "select") return;
6010
+ const { worldX, worldY } = screenToWorld(point.x, point.y);
6011
+ const request = resolveNativeSelectionContextMenuRequest({
6012
+ interactive,
6013
+ toolId: toolIdRef.current,
6014
+ selectedItems,
6015
+ worldPoint: { x: worldX, y: worldY },
6016
+ screenPoint: point
6017
+ });
6018
+ if (!request) return;
6019
+ contextMenuLongPressStartRef.current = point;
6020
+ contextMenuLongPressTimerRef.current = setTimeout(() => {
6021
+ contextMenuLongPressTimerRef.current = null;
6022
+ contextMenuLongPressStartRef.current = null;
6023
+ dragStateRef.current = { kind: "idle" };
6024
+ lastPanPoint.current = null;
6025
+ lastPinchDist.current = null;
6026
+ clearTransientItemsPreview();
6027
+ setRealtimePlacementPreview(null);
6028
+ setSelectionContextMenu(
6029
+ clampNativeContextMenuState(
6030
+ {
6031
+ ...request,
6032
+ canPaste: shapeClipboardRef.current !== null
6033
+ },
6034
+ size
6035
+ )
6036
+ );
6037
+ }, LONG_PRESS_CONTEXT_MENU_MS);
6038
+ },
6039
+ [
6040
+ clearNativeContextMenuLongPress,
6041
+ clearTransientItemsPreview,
6042
+ interactive,
6043
+ screenToWorld,
6044
+ selectedItems,
6045
+ setRealtimePlacementPreview,
6046
+ size
6047
+ ]
6048
+ );
6049
+ const cancelNativeContextMenuLongPressAfterMove = react.useCallback(
6050
+ (point) => {
6051
+ const start = contextMenuLongPressStartRef.current;
6052
+ if (!start) return;
6053
+ if (Math.hypot(point.x - start.x, point.y - start.y) > LONG_PRESS_CONTEXT_MENU_CANCEL_PX) {
6054
+ clearNativeContextMenuLongPress();
6055
+ }
6056
+ },
6057
+ [clearNativeContextMenuLongPress]
6058
+ );
5666
6059
  const notifyWorldPointerMove = react.useCallback(
5667
6060
  (point) => {
5668
6061
  const { worldX, worldY } = screenToWorld(point.x, point.y);
@@ -5685,10 +6078,6 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5685
6078
  }, []);
5686
6079
  const hideToolCursor = react.useCallback(() => {
5687
6080
  }, []);
5688
- const selectedItems = react.useMemo(
5689
- () => activeItems.filter((item) => selectedIds.includes(item.id)),
5690
- [activeItems, selectedIds]
5691
- );
5692
6081
  const selectedStyleInspectorState = react.useMemo(() => {
5693
6082
  const styleableItems = selectedItems.filter(
5694
6083
  (item) => !item.locked && getNativeStyleInspectorToolId(item) !== null
@@ -5733,10 +6122,77 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5733
6122
  },
5734
6123
  [patchCurrentStrokeStyle]
5735
6124
  );
6125
+ const copySelectedShapes = react.useCallback(() => {
6126
+ if (!interactive) return false;
6127
+ const clipboard = copyNativeSelectedShapeItems(
6128
+ itemsRef.current,
6129
+ selectedIdsRef.current
6130
+ );
6131
+ if (!clipboard) return false;
6132
+ shapeClipboardRef.current = clipboard;
6133
+ return true;
6134
+ }, [interactive]);
6135
+ const pasteCopiedShapes = react.useCallback(() => {
6136
+ if (!interactive) return false;
6137
+ const change = onItemsChangeRef.current;
6138
+ const clipboard = shapeClipboardRef.current;
6139
+ if (!change || !clipboard) return false;
6140
+ const pasted = pasteNativeShapeClipboard({
6141
+ items: itemsRef.current,
6142
+ clipboard
6143
+ });
6144
+ if (!pasted) return false;
6145
+ change(pasted.items);
6146
+ onSelectionChangeRef.current?.(pasted.selectedIds);
6147
+ return true;
6148
+ }, [interactive]);
6149
+ const duplicateSelectedShapes = react.useCallback(() => {
6150
+ if (!interactive) return false;
6151
+ const change = onItemsChangeRef.current;
6152
+ if (!change) return false;
6153
+ const duplicated = duplicateNativeSelectedShapes({
6154
+ items: itemsRef.current,
6155
+ selectedIds: selectedIdsRef.current
6156
+ });
6157
+ if (!duplicated) return false;
6158
+ change(duplicated.items);
6159
+ onSelectionChangeRef.current?.(duplicated.selectedIds);
6160
+ return true;
6161
+ }, [interactive]);
6162
+ const handleCopySelectedShapesFromMenu = react.useCallback(() => {
6163
+ copySelectedShapes();
6164
+ closeNativeSelectionContextMenu();
6165
+ }, [closeNativeSelectionContextMenu, copySelectedShapes]);
6166
+ const handlePasteCopiedShapesFromMenu = react.useCallback(() => {
6167
+ pasteCopiedShapes();
6168
+ closeNativeSelectionContextMenu();
6169
+ }, [closeNativeSelectionContextMenu, pasteCopiedShapes]);
6170
+ const handleDuplicateSelectedShapesFromMenu = react.useCallback(() => {
6171
+ duplicateSelectedShapes();
6172
+ closeNativeSelectionContextMenu();
6173
+ }, [closeNativeSelectionContextMenu, duplicateSelectedShapes]);
6174
+ const handleDeleteSelectedShapes = react.useCallback(() => {
6175
+ const menu = selectionContextMenuRef.current;
6176
+ const ids = menu?.itemIds ?? selectedIdsRef.current;
6177
+ if (ids.length === 0) return false;
6178
+ const change = onItemsChangeRef.current;
6179
+ if (!change) return false;
6180
+ const idSet = new Set(ids);
6181
+ const nextItems = itemsRef.current.filter((item) => !idSet.has(item.id));
6182
+ if (nextItems.length === itemsRef.current.length) return false;
6183
+ change(nextItems);
6184
+ onSelectionChangeRef.current?.(
6185
+ selectedIdsRef.current.filter((id) => !idSet.has(id))
6186
+ );
6187
+ closeNativeSelectionContextMenu();
6188
+ return true;
6189
+ }, [closeNativeSelectionContextMenu]);
5736
6190
  const lastPinchDist = react.useRef(null);
5737
6191
  const lastPanPoint = react.useRef(null);
5738
6192
  const beginDragAtScreenPoint = react.useCallback(
5739
6193
  (point) => {
6194
+ closeNativeSelectionContextMenu();
6195
+ startNativeContextMenuLongPress(point);
5740
6196
  lastPinchDist.current = null;
5741
6197
  lastPanPoint.current = null;
5742
6198
  const sx = point.x;
@@ -5950,15 +6406,18 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5950
6406
  dragStateRef.current = { kind: "pan" };
5951
6407
  },
5952
6408
  [
6409
+ closeNativeSelectionContextMenu,
5953
6410
  interactive,
5954
6411
  requestSelectToolAfterUse,
5955
6412
  screenToWorld,
5956
6413
  setRealtimePlacementPreview,
6414
+ startNativeContextMenuLongPress,
5957
6415
  updateToolCursorPoint
5958
6416
  ]
5959
6417
  );
5960
6418
  const applyDragMoveAtScreenPoint = react.useCallback(
5961
6419
  (point, pagePoint) => {
6420
+ cancelNativeContextMenuLongPressAfterMove(point);
5962
6421
  const cam = cameraRef.current;
5963
6422
  if (!cam) return;
5964
6423
  updateToolCursorPoint(point);
@@ -5984,19 +6443,22 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5984
6443
  const dx = worldX - (last?.x ?? worldX);
5985
6444
  const dy = worldY - (last?.y ?? worldY);
5986
6445
  const shouldAppendPoint = Math.hypot(dx, dy) > 0.5 / cam.zoom;
5987
- if (!shouldAppendPoint) return;
5988
- pts.push({ x: worldX, y: worldY });
6446
+ if (shouldAppendPoint) {
6447
+ pts.push({ x: worldX, y: worldY });
6448
+ }
5989
6449
  if (st.tool === "laser") {
5990
- setLaserTrail((prev) => [
5991
- ...prev,
5992
- { x: worldX, y: worldY, t: Date.now() }
5993
- ]);
6450
+ if (shouldAppendPoint) {
6451
+ setLaserTrail((prev) => [
6452
+ ...prev,
6453
+ { x: worldX, y: worldY, t: Date.now() }
6454
+ ]);
6455
+ }
5994
6456
  return;
5995
6457
  }
5996
6458
  setRealtimePlacementPreview({
5997
6459
  kind: "stroke",
5998
6460
  tool: st.tool,
5999
- points: pts.map((previewPoint) => ({ ...previewPoint })),
6461
+ points: [...pts],
6000
6462
  style: { ...strokeStyleRef.current }
6001
6463
  });
6002
6464
  return;
@@ -6094,6 +6556,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6094
6556
  }
6095
6557
  },
6096
6558
  [
6559
+ cancelNativeContextMenuLongPressAfterMove,
6097
6560
  requestRender,
6098
6561
  screenToWorld,
6099
6562
  setRealtimePlacementPreview,
@@ -6103,6 +6566,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6103
6566
  );
6104
6567
  const finishDragAtScreenPoint = react.useCallback(
6105
6568
  (point) => {
6569
+ clearNativeContextMenuLongPress();
6106
6570
  lastPinchDist.current = null;
6107
6571
  lastPanPoint.current = null;
6108
6572
  updateToolCursorPoint(point);
@@ -6350,6 +6814,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6350
6814
  dragStateRef.current = { kind: "idle" };
6351
6815
  },
6352
6816
  [
6817
+ clearNativeContextMenuLongPress,
6353
6818
  requestSelectToolAfterNativeLinkUse,
6354
6819
  requestSelectToolAfterUse,
6355
6820
  screenToWorld,
@@ -6393,6 +6858,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6393
6858
  const sx = evt.nativeEvent.locationX;
6394
6859
  const sy = evt.nativeEvent.locationY;
6395
6860
  if (touches && touches.length >= 2) {
6861
+ clearNativeContextMenuLongPress();
6396
6862
  hideToolCursor();
6397
6863
  notifyWorldPointerLeave();
6398
6864
  dragStateRef.current = { kind: "pan" };
@@ -6409,6 +6875,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6409
6875
  const pageX = evt.nativeEvent.pageX;
6410
6876
  const pageY = evt.nativeEvent.pageY;
6411
6877
  if (touches && touches.length >= 2) {
6878
+ clearNativeContextMenuLongPress();
6412
6879
  hideToolCursor();
6413
6880
  notifyWorldPointerLeave();
6414
6881
  const t0 = touches[0];
@@ -6439,6 +6906,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6439
6906
  });
6440
6907
  },
6441
6908
  onPanResponderTerminate: () => {
6909
+ clearNativeContextMenuLongPress();
6442
6910
  lastPinchDist.current = null;
6443
6911
  lastPanPoint.current = null;
6444
6912
  hideToolCursor();
@@ -6455,6 +6923,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6455
6923
  [
6456
6924
  applyDragMoveAtScreenPoint,
6457
6925
  beginDragAtScreenPoint,
6926
+ clearNativeContextMenuLongPress,
6458
6927
  finishDragAtScreenPoint,
6459
6928
  requestRender,
6460
6929
  hideToolCursor,
@@ -6480,9 +6949,18 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6480
6949
  options?.padding ?? 0.08
6481
6950
  );
6482
6951
  requestRender();
6483
- }
6952
+ },
6953
+ copySelectedShapes,
6954
+ pasteCopiedShapes,
6955
+ duplicateSelectedShapes
6484
6956
  }),
6485
- [requestRender, size]
6957
+ [
6958
+ copySelectedShapes,
6959
+ duplicateSelectedShapes,
6960
+ pasteCopiedShapes,
6961
+ requestRender,
6962
+ size
6963
+ ]
6486
6964
  );
6487
6965
  const activeStyleToolId = toolId === "draw" || toolId === "marker" ? toolId : null;
6488
6966
  const styleInspectorToolId = selectedStyleInspectorState?.toolId ?? activeStyleToolId;
@@ -6600,7 +7078,17 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6600
7078
  pointerEvents: "box-none",
6601
7079
  children: toolbar
6602
7080
  }
6603
- )
7081
+ ),
7082
+ interactive && selectionContextMenu ? /* @__PURE__ */ jsxRuntime.jsx(
7083
+ NativeSelectionContextMenu,
7084
+ {
7085
+ ...selectionContextMenu,
7086
+ onCopy: handleCopySelectedShapesFromMenu,
7087
+ onPaste: handlePasteCopiedShapesFromMenu,
7088
+ onDuplicate: handleDuplicateSelectedShapesFromMenu,
7089
+ onDelete: handleDeleteSelectedShapes
7090
+ }
7091
+ ) : null
6604
7092
  ] }),
6605
7093
  /* @__PURE__ */ jsxRuntime.jsx(
6606
7094
  reactNative.Modal,
@@ -6663,6 +7151,49 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6663
7151
  ] });
6664
7152
  });
6665
7153
  var styles4 = reactNative.StyleSheet.create({
7154
+ nativeSelectionContextMenu: {
7155
+ position: "absolute",
7156
+ width: NATIVE_CONTEXT_MENU_WIDTH,
7157
+ minHeight: NATIVE_CONTEXT_MENU_HEIGHT,
7158
+ flexDirection: "row",
7159
+ alignItems: "center",
7160
+ justifyContent: "space-between",
7161
+ padding: 4,
7162
+ borderRadius: 14,
7163
+ borderWidth: reactNative.StyleSheet.hairlineWidth,
7164
+ borderColor: "#d4d4d8",
7165
+ backgroundColor: "#ffffff",
7166
+ shadowColor: "#000000",
7167
+ shadowOpacity: 0.16,
7168
+ shadowRadius: 18,
7169
+ shadowOffset: { width: 0, height: 8 },
7170
+ elevation: NATIVE_VIEWPORT_OVERLAY_ELEVATION + 4,
7171
+ zIndex: NATIVE_VIEWPORT_OVERLAY_Z_INDEX + 4
7172
+ },
7173
+ nativeSelectionContextMenuButton: {
7174
+ minHeight: 42,
7175
+ alignItems: "center",
7176
+ justifyContent: "center",
7177
+ borderRadius: 10,
7178
+ paddingHorizontal: 10
7179
+ },
7180
+ nativeSelectionContextMenuButtonPressed: {
7181
+ backgroundColor: "#f4f4f5"
7182
+ },
7183
+ nativeSelectionContextMenuButtonDisabled: {
7184
+ opacity: 0.45
7185
+ },
7186
+ nativeSelectionContextMenuButtonText: {
7187
+ color: "#18181b",
7188
+ fontSize: 14,
7189
+ fontWeight: "700"
7190
+ },
7191
+ nativeSelectionContextMenuButtonTextDisabled: {
7192
+ color: "#71717a"
7193
+ },
7194
+ nativeSelectionContextMenuDeleteText: {
7195
+ color: "#dc2626"
7196
+ },
6666
7197
  nativeLinkDialogBackdrop: {
6667
7198
  flex: 1,
6668
7199
  alignItems: "center",