canvu-react 0.4.53 → 0.4.55

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,
@@ -3145,6 +3340,26 @@ function pointInNativeSelectedItemBounds(item, worldPoint) {
3145
3340
  );
3146
3341
  return local.x >= 0 && local.x <= bounds.width && local.y >= 0 && local.y <= bounds.height;
3147
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
+ }
3148
3363
  function resolveNativeCustomPlacement(toolId, customPlacement, customPlacements) {
3149
3364
  if (customPlacement?.toolId === toolId) return customPlacement;
3150
3365
  return customPlacements?.find((placement) => placement.toolId === toolId) ?? null;
@@ -5064,6 +5279,18 @@ function applyRotationFromPointer(item, startRotation, startPointerAngleRad, poi
5064
5279
  }
5065
5280
  return { ...item, rotation: startRotation + delta };
5066
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
+ }
5067
5294
  function resizeItemByHandle(item, start, handle, currentWorld) {
5068
5295
  const sb = normalizeRect(start.bounds);
5069
5296
  if (!itemAllowsResizeHandle(item, handle)) {
@@ -5288,6 +5515,39 @@ function hitTestNativeRemotePresence(peers, camera, point) {
5288
5515
  return null;
5289
5516
  }
5290
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
+
5291
5551
  // src/native/native-transient-items.ts
5292
5552
  function moveNativeTransientItems({
5293
5553
  items,
@@ -5336,6 +5596,11 @@ var NATIVE_VIEWPORT_OVERLAY_Z_INDEX = 40;
5336
5596
  var NATIVE_VIEWPORT_OVERLAY_ELEVATION = 40;
5337
5597
  var NATIVE_VIEWPORT_EXTERNAL_OVERLAY_Z_INDEX = 20;
5338
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 = 168;
5602
+ var NATIVE_CONTEXT_MENU_HEIGHT = 184;
5603
+ var NATIVE_CONTEXT_MENU_MARGIN = 12;
5339
5604
  function isPlacementTool(toolId) {
5340
5605
  return toolId === "rect" || toolId === "ellipse" || toolId === "architectural-cloud" || toolId === "line" || toolId === "arrow";
5341
5606
  }
@@ -5408,6 +5673,102 @@ function fitCameraToWorldRect(camera, viewportW, viewportH, worldRect, padding)
5408
5673
  camera.x = viewportW / 2 - cx * z;
5409
5674
  camera.y = viewportH / 2 - cy * z;
5410
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
+ }
5411
5772
  var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5412
5773
  items,
5413
5774
  selectedIds = [],
@@ -5474,6 +5835,16 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5474
5835
  selectedIdsRef.current = selectedIds;
5475
5836
  const remotePresenceRef = react.useRef(remotePresence);
5476
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);
5477
5848
  const dragStateRef = react.useRef({ kind: "idle" });
5478
5849
  react.useEffect(() => {
5479
5850
  const committedItems = items;
@@ -5491,6 +5862,16 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5491
5862
  transientItemsRef.current = null;
5492
5863
  setTransientItems(null);
5493
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
+ }, []);
5494
5875
  const commitTransientItemsPreview = react.useCallback(() => {
5495
5876
  const nextItems = transientItemsRef.current;
5496
5877
  const change = onItemsChangeRef.current;
@@ -5523,6 +5904,9 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5523
5904
  if (laserClearTimerRef.current) {
5524
5905
  clearTimeout(laserClearTimerRef.current);
5525
5906
  }
5907
+ if (contextMenuLongPressTimerRef.current) {
5908
+ clearTimeout(contextMenuLongPressTimerRef.current);
5909
+ }
5526
5910
  },
5527
5911
  []
5528
5912
  );
@@ -5607,6 +5991,10 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5607
5991
  }
5608
5992
  const camera = cameraRef.current;
5609
5993
  const [cameraTick, setCameraTick] = react.useState(0);
5994
+ const selectedItems = react.useMemo(
5995
+ () => activeItems.filter((item) => selectedIds.includes(item.id)),
5996
+ [activeItems, selectedIds]
5997
+ );
5610
5998
  const screenToWorld = react.useCallback(
5611
5999
  (sx, sy) => {
5612
6000
  const cam = cameraRef.current;
@@ -5615,6 +6003,59 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5615
6003
  },
5616
6004
  []
5617
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
+ );
5618
6059
  const notifyWorldPointerMove = react.useCallback(
5619
6060
  (point) => {
5620
6061
  const { worldX, worldY } = screenToWorld(point.x, point.y);
@@ -5637,10 +6078,6 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5637
6078
  }, []);
5638
6079
  const hideToolCursor = react.useCallback(() => {
5639
6080
  }, []);
5640
- const selectedItems = react.useMemo(
5641
- () => activeItems.filter((item) => selectedIds.includes(item.id)),
5642
- [activeItems, selectedIds]
5643
- );
5644
6081
  const selectedStyleInspectorState = react.useMemo(() => {
5645
6082
  const styleableItems = selectedItems.filter(
5646
6083
  (item) => !item.locked && getNativeStyleInspectorToolId(item) !== null
@@ -5685,10 +6122,77 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5685
6122
  },
5686
6123
  [patchCurrentStrokeStyle]
5687
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]);
5688
6190
  const lastPinchDist = react.useRef(null);
5689
6191
  const lastPanPoint = react.useRef(null);
5690
6192
  const beginDragAtScreenPoint = react.useCallback(
5691
6193
  (point) => {
6194
+ closeNativeSelectionContextMenu();
6195
+ startNativeContextMenuLongPress(point);
5692
6196
  lastPinchDist.current = null;
5693
6197
  lastPanPoint.current = null;
5694
6198
  const sx = point.x;
@@ -5902,15 +6406,18 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
5902
6406
  dragStateRef.current = { kind: "pan" };
5903
6407
  },
5904
6408
  [
6409
+ closeNativeSelectionContextMenu,
5905
6410
  interactive,
5906
6411
  requestSelectToolAfterUse,
5907
6412
  screenToWorld,
5908
6413
  setRealtimePlacementPreview,
6414
+ startNativeContextMenuLongPress,
5909
6415
  updateToolCursorPoint
5910
6416
  ]
5911
6417
  );
5912
6418
  const applyDragMoveAtScreenPoint = react.useCallback(
5913
6419
  (point, pagePoint) => {
6420
+ cancelNativeContextMenuLongPressAfterMove(point);
5914
6421
  const cam = cameraRef.current;
5915
6422
  if (!cam) return;
5916
6423
  updateToolCursorPoint(point);
@@ -6049,6 +6556,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6049
6556
  }
6050
6557
  },
6051
6558
  [
6559
+ cancelNativeContextMenuLongPressAfterMove,
6052
6560
  requestRender,
6053
6561
  screenToWorld,
6054
6562
  setRealtimePlacementPreview,
@@ -6058,6 +6566,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6058
6566
  );
6059
6567
  const finishDragAtScreenPoint = react.useCallback(
6060
6568
  (point) => {
6569
+ clearNativeContextMenuLongPress();
6061
6570
  lastPinchDist.current = null;
6062
6571
  lastPanPoint.current = null;
6063
6572
  updateToolCursorPoint(point);
@@ -6305,6 +6814,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6305
6814
  dragStateRef.current = { kind: "idle" };
6306
6815
  },
6307
6816
  [
6817
+ clearNativeContextMenuLongPress,
6308
6818
  requestSelectToolAfterNativeLinkUse,
6309
6819
  requestSelectToolAfterUse,
6310
6820
  screenToWorld,
@@ -6348,6 +6858,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6348
6858
  const sx = evt.nativeEvent.locationX;
6349
6859
  const sy = evt.nativeEvent.locationY;
6350
6860
  if (touches && touches.length >= 2) {
6861
+ clearNativeContextMenuLongPress();
6351
6862
  hideToolCursor();
6352
6863
  notifyWorldPointerLeave();
6353
6864
  dragStateRef.current = { kind: "pan" };
@@ -6364,6 +6875,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6364
6875
  const pageX = evt.nativeEvent.pageX;
6365
6876
  const pageY = evt.nativeEvent.pageY;
6366
6877
  if (touches && touches.length >= 2) {
6878
+ clearNativeContextMenuLongPress();
6367
6879
  hideToolCursor();
6368
6880
  notifyWorldPointerLeave();
6369
6881
  const t0 = touches[0];
@@ -6394,6 +6906,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6394
6906
  });
6395
6907
  },
6396
6908
  onPanResponderTerminate: () => {
6909
+ clearNativeContextMenuLongPress();
6397
6910
  lastPinchDist.current = null;
6398
6911
  lastPanPoint.current = null;
6399
6912
  hideToolCursor();
@@ -6410,6 +6923,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6410
6923
  [
6411
6924
  applyDragMoveAtScreenPoint,
6412
6925
  beginDragAtScreenPoint,
6926
+ clearNativeContextMenuLongPress,
6413
6927
  finishDragAtScreenPoint,
6414
6928
  requestRender,
6415
6929
  hideToolCursor,
@@ -6435,9 +6949,18 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6435
6949
  options?.padding ?? 0.08
6436
6950
  );
6437
6951
  requestRender();
6438
- }
6952
+ },
6953
+ copySelectedShapes,
6954
+ pasteCopiedShapes,
6955
+ duplicateSelectedShapes
6439
6956
  }),
6440
- [requestRender, size]
6957
+ [
6958
+ copySelectedShapes,
6959
+ duplicateSelectedShapes,
6960
+ pasteCopiedShapes,
6961
+ requestRender,
6962
+ size
6963
+ ]
6441
6964
  );
6442
6965
  const activeStyleToolId = toolId === "draw" || toolId === "marker" ? toolId : null;
6443
6966
  const styleInspectorToolId = selectedStyleInspectorState?.toolId ?? activeStyleToolId;
@@ -6555,7 +7078,17 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6555
7078
  pointerEvents: "box-none",
6556
7079
  children: toolbar
6557
7080
  }
6558
- )
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
6559
7092
  ] }),
6560
7093
  /* @__PURE__ */ jsxRuntime.jsx(
6561
7094
  reactNative.Modal,
@@ -6618,6 +7151,50 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
6618
7151
  ] });
6619
7152
  });
6620
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: "column",
7159
+ alignItems: "stretch",
7160
+ justifyContent: "flex-start",
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
+ width: "100%",
7175
+ minHeight: 42,
7176
+ alignItems: "flex-start",
7177
+ justifyContent: "center",
7178
+ borderRadius: 10,
7179
+ paddingHorizontal: 14
7180
+ },
7181
+ nativeSelectionContextMenuButtonPressed: {
7182
+ backgroundColor: "#f4f4f5"
7183
+ },
7184
+ nativeSelectionContextMenuButtonDisabled: {
7185
+ opacity: 0.45
7186
+ },
7187
+ nativeSelectionContextMenuButtonText: {
7188
+ color: "#18181b",
7189
+ fontSize: 14,
7190
+ fontWeight: "400"
7191
+ },
7192
+ nativeSelectionContextMenuButtonTextDisabled: {
7193
+ color: "#71717a"
7194
+ },
7195
+ nativeSelectionContextMenuDeleteText: {
7196
+ color: "#dc2626"
7197
+ },
6621
7198
  nativeLinkDialogBackdrop: {
6622
7199
  flex: 1,
6623
7200
  alignItems: "center",