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.js CHANGED
@@ -1193,6 +1193,9 @@ function cloneVectorSceneItemWithNewId(item) {
1193
1193
  }
1194
1194
  return next;
1195
1195
  }
1196
+ function cloneVectorSceneItemsWithNewIds(items) {
1197
+ return items.map(cloneVectorSceneItemWithNewId);
1198
+ }
1196
1199
 
1197
1200
  // src/scene/managed-images.ts
1198
1201
  var MANAGED_KEY = "managed";
@@ -1256,8 +1259,68 @@ function rotateManagedImage(items, id) {
1256
1259
  function deleteManagedImage(items, id) {
1257
1260
  return restackManagedImages(items.filter((i) => i.id !== id));
1258
1261
  }
1262
+ function reorderManagedImages(items, orderedManagedIds) {
1263
+ const managedSlots = [];
1264
+ for (let i = 0; i < items.length; i++) {
1265
+ const item = items[i];
1266
+ if (item && isManagedImage(item)) managedSlots.push(i);
1267
+ }
1268
+ if (managedSlots.length !== orderedManagedIds.length) {
1269
+ return [...items];
1270
+ }
1271
+ const byId = new Map(items.map((i) => [i.id, i]));
1272
+ const next = [...items];
1273
+ managedSlots.forEach((slot, k) => {
1274
+ const orderedId = orderedManagedIds[k];
1275
+ if (orderedId === void 0) return;
1276
+ const replacement = byId.get(orderedId);
1277
+ if (replacement) next[slot] = replacement;
1278
+ });
1279
+ return restackManagedImages(next);
1280
+ }
1281
+
1282
+ // src/native/native-images-menu-reorder.ts
1283
+ function moveNativeManagedImageId(managedIds, fromIndex, toIndex) {
1284
+ const next = [...managedIds];
1285
+ if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= next.length || toIndex >= next.length) {
1286
+ return next;
1287
+ }
1288
+ const [item] = next.splice(fromIndex, 1);
1289
+ if (item === void 0) return next;
1290
+ next.splice(toIndex, 0, item);
1291
+ return next;
1292
+ }
1293
+ function getNativeImagesMenuReorderIndex({
1294
+ activeId,
1295
+ managedIds,
1296
+ rowLayouts,
1297
+ dragDeltaY
1298
+ }) {
1299
+ const currentIndex = managedIds.indexOf(activeId);
1300
+ if (currentIndex < 0) return -1;
1301
+ const layoutById = new Map(rowLayouts.map((layout) => [layout.id, layout]));
1302
+ const activeLayout = layoutById.get(activeId);
1303
+ if (!activeLayout) return currentIndex;
1304
+ const activeCenterY = activeLayout.y + activeLayout.height / 2 + dragDeltaY;
1305
+ let nextIndex = currentIndex;
1306
+ let closestDistance = Infinity;
1307
+ for (let index = 0; index < managedIds.length; index++) {
1308
+ const id = managedIds[index];
1309
+ if (id === void 0) continue;
1310
+ const layout = layoutById.get(id);
1311
+ if (!layout) continue;
1312
+ const centerY = layout.y + layout.height / 2;
1313
+ const distance = Math.abs(activeCenterY - centerY);
1314
+ if (distance < closestDistance) {
1315
+ closestDistance = distance;
1316
+ nextIndex = index;
1317
+ }
1318
+ }
1319
+ return nextIndex;
1320
+ }
1259
1321
  var defaultLabels = {
1260
1322
  title: "Images",
1323
+ dragHandle: "Drag to reorder",
1261
1324
  focus: "Focus on canvas",
1262
1325
  duplicate: "Duplicate",
1263
1326
  rotate: "Rotate",
@@ -1329,61 +1392,110 @@ function NativeImagesMenuRow({
1329
1392
  item,
1330
1393
  items,
1331
1394
  labels,
1395
+ isDragging,
1396
+ isDropTarget,
1397
+ dragDeltaY,
1332
1398
  onItemsChange,
1333
1399
  onFocusItem,
1400
+ onDragStart,
1401
+ onDragMove,
1402
+ onDragEnd,
1403
+ onRowLayout,
1334
1404
  getImageUri,
1335
1405
  renderThumbnail,
1336
1406
  rowStyle,
1337
1407
  actionButtonStyle
1338
1408
  }) {
1339
1409
  const imageUri = getImageUri(item);
1340
- return /* @__PURE__ */ jsxs(View, { style: [styles.row, rowStyle], children: [
1341
- /* @__PURE__ */ jsx(View, { style: styles.handle, children: /* @__PURE__ */ jsx(NativeImagesMenuIcon, { name: "grip", color: "#94a3b8" }) }),
1342
- /* @__PURE__ */ jsx(
1343
- Pressable,
1344
- {
1345
- accessibilityRole: "button",
1346
- accessibilityLabel: labels.focus,
1347
- disabled: !onFocusItem,
1348
- onPress: () => onFocusItem?.(item),
1349
- style: styles.thumbnailButton,
1350
- children: /* @__PURE__ */ jsxs(View, { style: styles.thumbnailBox, children: [
1351
- renderThumbnail ? renderThumbnail(item) : null,
1352
- !renderThumbnail && imageUri ? /* @__PURE__ */ jsx(Image$1, { source: { uri: imageUri }, style: styles.thumbnailImage }) : null
1410
+ const panResponder = useMemo(
1411
+ () => PanResponder.create({
1412
+ onStartShouldSetPanResponder: () => true,
1413
+ onMoveShouldSetPanResponder: () => true,
1414
+ onPanResponderGrant: () => onDragStart(item.id),
1415
+ onPanResponderMove: (_event, gestureState) => onDragMove(item.id, gestureState.dy),
1416
+ onPanResponderRelease: (_event, gestureState) => onDragEnd(item.id, gestureState.dy),
1417
+ onPanResponderTerminate: (_event, gestureState) => onDragEnd(item.id, gestureState.dy),
1418
+ onPanResponderTerminationRequest: () => false,
1419
+ onShouldBlockNativeResponder: () => true
1420
+ }),
1421
+ [item.id, onDragEnd, onDragMove, onDragStart]
1422
+ );
1423
+ const onLayout = useCallback(
1424
+ (event) => {
1425
+ const { height, y } = event.nativeEvent.layout;
1426
+ onRowLayout({ id: item.id, y, height });
1427
+ },
1428
+ [item.id, onRowLayout]
1429
+ );
1430
+ return /* @__PURE__ */ jsxs(
1431
+ View,
1432
+ {
1433
+ onLayout,
1434
+ style: [
1435
+ styles.row,
1436
+ rowStyle,
1437
+ isDropTarget && styles.dropTargetRow,
1438
+ isDragging && styles.draggingRow,
1439
+ isDragging && { transform: [{ translateY: dragDeltaY }] }
1440
+ ],
1441
+ children: [
1442
+ /* @__PURE__ */ jsx(
1443
+ View,
1444
+ {
1445
+ accessibilityRole: "button",
1446
+ accessibilityLabel: labels.dragHandle,
1447
+ style: [styles.handle, isDragging && styles.handleDragging],
1448
+ ...panResponder.panHandlers,
1449
+ children: /* @__PURE__ */ jsx(NativeImagesMenuIcon, { name: "grip", color: "#94a3b8" })
1450
+ }
1451
+ ),
1452
+ /* @__PURE__ */ jsx(
1453
+ Pressable,
1454
+ {
1455
+ accessibilityRole: "button",
1456
+ accessibilityLabel: labels.focus,
1457
+ disabled: !onFocusItem,
1458
+ onPress: () => onFocusItem?.(item),
1459
+ style: styles.thumbnailButton,
1460
+ children: /* @__PURE__ */ jsxs(View, { style: styles.thumbnailBox, children: [
1461
+ renderThumbnail ? renderThumbnail(item) : null,
1462
+ !renderThumbnail && imageUri ? /* @__PURE__ */ jsx(Image$1, { source: { uri: imageUri }, style: styles.thumbnailImage }) : null
1463
+ ] })
1464
+ }
1465
+ ),
1466
+ /* @__PURE__ */ jsxs(View, { style: styles.actionsColumn, children: [
1467
+ /* @__PURE__ */ jsx(
1468
+ NativeImagesMenuAction,
1469
+ {
1470
+ label: labels.duplicate,
1471
+ onPress: () => onItemsChange(copyManagedImage(items, item.id)),
1472
+ style: actionButtonStyle,
1473
+ children: /* @__PURE__ */ jsx(NativeImagesMenuIcon, { name: "copy" })
1474
+ }
1475
+ ),
1476
+ /* @__PURE__ */ jsx(
1477
+ NativeImagesMenuAction,
1478
+ {
1479
+ label: labels.rotate,
1480
+ onPress: () => onItemsChange(rotateManagedImage(items, item.id)),
1481
+ style: actionButtonStyle,
1482
+ children: /* @__PURE__ */ jsx(NativeImagesMenuIcon, { name: "rotate" })
1483
+ }
1484
+ ),
1485
+ /* @__PURE__ */ jsx(
1486
+ NativeImagesMenuAction,
1487
+ {
1488
+ label: labels.delete,
1489
+ danger: true,
1490
+ onPress: () => onItemsChange(deleteManagedImage(items, item.id)),
1491
+ style: actionButtonStyle,
1492
+ children: /* @__PURE__ */ jsx(NativeImagesMenuIcon, { name: "trash", color: "#b91c1c" })
1493
+ }
1494
+ )
1353
1495
  ] })
1354
- }
1355
- ),
1356
- /* @__PURE__ */ jsxs(View, { style: styles.actionsColumn, children: [
1357
- /* @__PURE__ */ jsx(
1358
- NativeImagesMenuAction,
1359
- {
1360
- label: labels.duplicate,
1361
- onPress: () => onItemsChange(copyManagedImage(items, item.id)),
1362
- style: actionButtonStyle,
1363
- children: /* @__PURE__ */ jsx(NativeImagesMenuIcon, { name: "copy" })
1364
- }
1365
- ),
1366
- /* @__PURE__ */ jsx(
1367
- NativeImagesMenuAction,
1368
- {
1369
- label: labels.rotate,
1370
- onPress: () => onItemsChange(rotateManagedImage(items, item.id)),
1371
- style: actionButtonStyle,
1372
- children: /* @__PURE__ */ jsx(NativeImagesMenuIcon, { name: "rotate" })
1373
- }
1374
- ),
1375
- /* @__PURE__ */ jsx(
1376
- NativeImagesMenuAction,
1377
- {
1378
- label: labels.delete,
1379
- danger: true,
1380
- onPress: () => onItemsChange(deleteManagedImage(items, item.id)),
1381
- style: actionButtonStyle,
1382
- children: /* @__PURE__ */ jsx(NativeImagesMenuIcon, { name: "trash", color: "#b91c1c" })
1383
- }
1384
- )
1385
- ] })
1386
- ] });
1496
+ ]
1497
+ }
1498
+ );
1387
1499
  }
1388
1500
  function NativeImagesMenu({
1389
1501
  items,
@@ -1400,7 +1512,69 @@ function NativeImagesMenu({
1400
1512
  renderThumbnail
1401
1513
  }) {
1402
1514
  const managed = useMemo(() => items.filter(isManagedImage), [items]);
1515
+ const managedIds = useMemo(() => managed.map((item) => item.id), [managed]);
1403
1516
  const [collapsed, setCollapsed] = useState(!defaultOpen);
1517
+ const [dragState, setDragState] = useState(null);
1518
+ const rowLayoutsRef = useRef(/* @__PURE__ */ new Map());
1519
+ const getRowLayouts = useCallback(
1520
+ () => managedIds.map((id) => rowLayoutsRef.current.get(id)).filter((layout) => layout != null),
1521
+ [managedIds]
1522
+ );
1523
+ const handleRowLayout = useCallback((layout) => {
1524
+ rowLayoutsRef.current.set(layout.id, layout);
1525
+ }, []);
1526
+ const handleDragStart = useCallback(
1527
+ (id) => {
1528
+ const fromIndex = managedIds.indexOf(id);
1529
+ if (fromIndex < 0) return;
1530
+ setDragState({
1531
+ activeId: id,
1532
+ fromIndex,
1533
+ currentIndex: fromIndex,
1534
+ dragDeltaY: 0
1535
+ });
1536
+ },
1537
+ [managedIds]
1538
+ );
1539
+ const handleDragMove = useCallback(
1540
+ (id, dragDeltaY) => {
1541
+ setDragState((current) => {
1542
+ if (!current || current.activeId !== id) return current;
1543
+ const currentIndex = getNativeImagesMenuReorderIndex({
1544
+ activeId: id,
1545
+ managedIds,
1546
+ rowLayouts: getRowLayouts(),
1547
+ dragDeltaY
1548
+ });
1549
+ return {
1550
+ ...current,
1551
+ currentIndex: currentIndex >= 0 ? currentIndex : current.fromIndex,
1552
+ dragDeltaY
1553
+ };
1554
+ });
1555
+ },
1556
+ [getRowLayouts, managedIds]
1557
+ );
1558
+ const handleDragEnd = useCallback(
1559
+ (id, dragDeltaY) => {
1560
+ const fromIndex = managedIds.indexOf(id);
1561
+ const currentIndex = getNativeImagesMenuReorderIndex({
1562
+ activeId: id,
1563
+ managedIds,
1564
+ rowLayouts: getRowLayouts(),
1565
+ dragDeltaY
1566
+ });
1567
+ setDragState(null);
1568
+ if (fromIndex < 0 || currentIndex < 0 || fromIndex === currentIndex) return;
1569
+ const orderedIds = moveNativeManagedImageId(
1570
+ managedIds,
1571
+ fromIndex,
1572
+ currentIndex
1573
+ );
1574
+ onItemsChange(reorderManagedImages(items, orderedIds));
1575
+ },
1576
+ [getRowLayouts, items, managedIds, onItemsChange]
1577
+ );
1404
1578
  if (managed.length === 0) return null;
1405
1579
  const resolvedLabels = resolveLabels(labels);
1406
1580
  if (collapsed) {
@@ -1445,15 +1619,23 @@ function NativeImagesMenu({
1445
1619
  {
1446
1620
  style: styles.listScroll,
1447
1621
  contentContainerStyle: styles.listContent,
1622
+ scrollEnabled: dragState == null,
1448
1623
  showsVerticalScrollIndicator: false,
1449
- children: managed.map((item) => /* @__PURE__ */ jsx(
1624
+ children: managed.map((item, index) => /* @__PURE__ */ jsx(
1450
1625
  NativeImagesMenuRow,
1451
1626
  {
1452
1627
  item,
1453
1628
  items,
1454
1629
  labels: resolvedLabels,
1630
+ isDragging: dragState?.activeId === item.id,
1631
+ isDropTarget: dragState != null && dragState.activeId !== item.id && dragState.currentIndex === index,
1632
+ dragDeltaY: dragState?.activeId === item.id ? dragState.dragDeltaY : 0,
1455
1633
  onItemsChange,
1456
1634
  onFocusItem,
1635
+ onDragStart: handleDragStart,
1636
+ onDragMove: handleDragMove,
1637
+ onDragEnd: handleDragEnd,
1638
+ onRowLayout: handleRowLayout,
1457
1639
  getImageUri,
1458
1640
  renderThumbnail,
1459
1641
  rowStyle,
@@ -1513,6 +1695,19 @@ var styles = StyleSheet.create({
1513
1695
  dangerActionButton: {
1514
1696
  backgroundColor: "#fef2f2"
1515
1697
  },
1698
+ draggingRow: {
1699
+ backgroundColor: "#eef2f7",
1700
+ elevation: 4,
1701
+ opacity: 0.85,
1702
+ zIndex: 1
1703
+ },
1704
+ dropTargetRow: {
1705
+ backgroundColor: "#f8fafc"
1706
+ },
1707
+ handleDragging: {
1708
+ backgroundColor: "#e2e8f0",
1709
+ borderRadius: 6
1710
+ },
1516
1711
  handle: {
1517
1712
  alignItems: "center",
1518
1713
  height: 128,
@@ -3139,6 +3334,26 @@ function pointInNativeSelectedItemBounds(item, worldPoint) {
3139
3334
  );
3140
3335
  return local.x >= 0 && local.x <= bounds.width && local.y >= 0 && local.y <= bounds.height;
3141
3336
  }
3337
+ function resolveNativeSelectionContextMenuRequest({
3338
+ interactive,
3339
+ toolId,
3340
+ selectedItems,
3341
+ worldPoint,
3342
+ screenPoint
3343
+ }) {
3344
+ if (!interactive || toolId !== "select") return null;
3345
+ const editableSelectedItems = selectedItems.filter((item) => !item.locked);
3346
+ if (editableSelectedItems.length === 0) return null;
3347
+ const pressedSelectedItem = editableSelectedItems.some(
3348
+ (item) => pointInNativeSelectedItemBounds(item, worldPoint)
3349
+ );
3350
+ if (!pressedSelectedItem) return null;
3351
+ return {
3352
+ itemIds: editableSelectedItems.map((item) => item.id),
3353
+ x: screenPoint.x,
3354
+ y: screenPoint.y
3355
+ };
3356
+ }
3142
3357
  function resolveNativeCustomPlacement(toolId, customPlacement, customPlacements) {
3143
3358
  if (customPlacement?.toolId === toolId) return customPlacement;
3144
3359
  return customPlacements?.find((placement) => placement.toolId === toolId) ?? null;
@@ -5058,6 +5273,18 @@ function applyRotationFromPointer(item, startRotation, startPointerAngleRad, poi
5058
5273
  }
5059
5274
  return { ...item, rotation: startRotation + delta };
5060
5275
  }
5276
+ function moveItemByDelta(item, dx, dy) {
5277
+ return {
5278
+ ...item,
5279
+ x: item.x + dx,
5280
+ y: item.y + dy,
5281
+ bounds: {
5282
+ ...item.bounds,
5283
+ x: item.bounds.x + dx,
5284
+ y: item.bounds.y + dy
5285
+ }
5286
+ };
5287
+ }
5061
5288
  function resizeItemByHandle(item, start, handle, currentWorld) {
5062
5289
  const sb = normalizeRect(start.bounds);
5063
5290
  if (!itemAllowsResizeHandle(item, handle)) {
@@ -5282,6 +5509,39 @@ function hitTestNativeRemotePresence(peers, camera, point) {
5282
5509
  return null;
5283
5510
  }
5284
5511
 
5512
+ // src/native/native-shape-clipboard.ts
5513
+ var NATIVE_SHAPE_CLIPBOARD_PASTE_OFFSET_WORLD = 24;
5514
+ function copyNativeSelectedShapeItems(items, selectedIds) {
5515
+ if (selectedIds.length === 0) return null;
5516
+ const copies = selectedIds.map((id) => items.find((item) => item.id === id)).filter((item) => item != null);
5517
+ if (copies.length === 0) return null;
5518
+ return copies.map((item) => JSON.parse(JSON.stringify(item)));
5519
+ }
5520
+ function pasteNativeShapeClipboard(input) {
5521
+ if (input.clipboard.length === 0) return null;
5522
+ const clones = cloneVectorSceneItemsWithNewIds(input.clipboard);
5523
+ const moved = clones.map(
5524
+ (item) => moveItemByDelta(
5525
+ item,
5526
+ NATIVE_SHAPE_CLIPBOARD_PASTE_OFFSET_WORLD,
5527
+ NATIVE_SHAPE_CLIPBOARD_PASTE_OFFSET_WORLD
5528
+ )
5529
+ );
5530
+ if (moved.length === 0) return null;
5531
+ return {
5532
+ items: [...input.items, ...moved],
5533
+ selectedIds: moved.map((item) => item.id)
5534
+ };
5535
+ }
5536
+ function duplicateNativeSelectedShapes(input) {
5537
+ const clipboard = copyNativeSelectedShapeItems(input.items, input.selectedIds);
5538
+ if (!clipboard) return null;
5539
+ return pasteNativeShapeClipboard({
5540
+ items: input.items,
5541
+ clipboard
5542
+ });
5543
+ }
5544
+
5285
5545
  // src/native/native-transient-items.ts
5286
5546
  function moveNativeTransientItems({
5287
5547
  items,
@@ -5330,6 +5590,11 @@ var NATIVE_VIEWPORT_OVERLAY_Z_INDEX = 40;
5330
5590
  var NATIVE_VIEWPORT_OVERLAY_ELEVATION = 40;
5331
5591
  var NATIVE_VIEWPORT_EXTERNAL_OVERLAY_Z_INDEX = 20;
5332
5592
  var NATIVE_VIEWPORT_EXTERNAL_OVERLAY_ELEVATION = 20;
5593
+ var LONG_PRESS_CONTEXT_MENU_MS = 520;
5594
+ var LONG_PRESS_CONTEXT_MENU_CANCEL_PX = 10;
5595
+ var NATIVE_CONTEXT_MENU_WIDTH = 168;
5596
+ var NATIVE_CONTEXT_MENU_HEIGHT = 184;
5597
+ var NATIVE_CONTEXT_MENU_MARGIN = 12;
5333
5598
  function isPlacementTool(toolId) {
5334
5599
  return toolId === "rect" || toolId === "ellipse" || toolId === "architectural-cloud" || toolId === "line" || toolId === "arrow";
5335
5600
  }
@@ -5402,6 +5667,102 @@ function fitCameraToWorldRect(camera, viewportW, viewportH, worldRect, padding)
5402
5667
  camera.x = viewportW / 2 - cx * z;
5403
5668
  camera.y = viewportH / 2 - cy * z;
5404
5669
  }
5670
+ function clampNativeContextMenuState(menu, size) {
5671
+ return {
5672
+ ...menu,
5673
+ x: Math.max(
5674
+ NATIVE_CONTEXT_MENU_MARGIN,
5675
+ Math.min(
5676
+ menu.x,
5677
+ Math.max(NATIVE_CONTEXT_MENU_MARGIN, size.width - NATIVE_CONTEXT_MENU_WIDTH)
5678
+ )
5679
+ ),
5680
+ y: Math.max(
5681
+ NATIVE_CONTEXT_MENU_MARGIN,
5682
+ Math.min(
5683
+ menu.y - NATIVE_CONTEXT_MENU_HEIGHT - NATIVE_CONTEXT_MENU_MARGIN,
5684
+ Math.max(
5685
+ NATIVE_CONTEXT_MENU_MARGIN,
5686
+ size.height - NATIVE_CONTEXT_MENU_HEIGHT - NATIVE_CONTEXT_MENU_MARGIN
5687
+ )
5688
+ )
5689
+ )
5690
+ };
5691
+ }
5692
+ function NativeSelectionContextMenu({
5693
+ x,
5694
+ y,
5695
+ canPaste,
5696
+ onCopy,
5697
+ onPaste,
5698
+ onDuplicate,
5699
+ onDelete
5700
+ }) {
5701
+ return /* @__PURE__ */ jsxs(
5702
+ View,
5703
+ {
5704
+ style: [
5705
+ styles4.nativeSelectionContextMenu,
5706
+ {
5707
+ left: x,
5708
+ top: y
5709
+ }
5710
+ ],
5711
+ children: [
5712
+ /* @__PURE__ */ jsx(NativeSelectionContextMenuButton, { label: "Copy", onPress: onCopy }),
5713
+ /* @__PURE__ */ jsx(
5714
+ NativeSelectionContextMenuButton,
5715
+ {
5716
+ label: "Paste",
5717
+ onPress: onPaste,
5718
+ disabled: !canPaste
5719
+ }
5720
+ ),
5721
+ /* @__PURE__ */ jsx(NativeSelectionContextMenuButton, { label: "Duplicate", onPress: onDuplicate }),
5722
+ /* @__PURE__ */ jsx(
5723
+ NativeSelectionContextMenuButton,
5724
+ {
5725
+ label: "Delete",
5726
+ onPress: onDelete,
5727
+ destructive: true
5728
+ }
5729
+ )
5730
+ ]
5731
+ }
5732
+ );
5733
+ }
5734
+ function NativeSelectionContextMenuButton({
5735
+ label,
5736
+ onPress,
5737
+ disabled = false,
5738
+ destructive = false
5739
+ }) {
5740
+ return /* @__PURE__ */ jsx(
5741
+ Pressable,
5742
+ {
5743
+ accessibilityRole: "button",
5744
+ accessibilityState: { disabled },
5745
+ disabled,
5746
+ onPress,
5747
+ style: ({ pressed }) => [
5748
+ styles4.nativeSelectionContextMenuButton,
5749
+ pressed && !disabled ? styles4.nativeSelectionContextMenuButtonPressed : void 0,
5750
+ disabled ? styles4.nativeSelectionContextMenuButtonDisabled : void 0
5751
+ ],
5752
+ children: /* @__PURE__ */ jsx(
5753
+ Text,
5754
+ {
5755
+ style: [
5756
+ styles4.nativeSelectionContextMenuButtonText,
5757
+ destructive ? styles4.nativeSelectionContextMenuDeleteText : void 0,
5758
+ disabled ? styles4.nativeSelectionContextMenuButtonTextDisabled : void 0
5759
+ ],
5760
+ children: label
5761
+ }
5762
+ )
5763
+ }
5764
+ );
5765
+ }
5405
5766
  var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5406
5767
  items,
5407
5768
  selectedIds = [],
@@ -5468,6 +5829,16 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5468
5829
  selectedIdsRef.current = selectedIds;
5469
5830
  const remotePresenceRef = useRef(remotePresence);
5470
5831
  remotePresenceRef.current = remotePresence;
5832
+ const shapeClipboardRef = useRef(null);
5833
+ const [selectionContextMenu, setSelectionContextMenu] = useState(null);
5834
+ const selectionContextMenuRef = useRef(
5835
+ null
5836
+ );
5837
+ selectionContextMenuRef.current = selectionContextMenu;
5838
+ const contextMenuLongPressTimerRef = useRef(
5839
+ null
5840
+ );
5841
+ const contextMenuLongPressStartRef = useRef(null);
5471
5842
  const dragStateRef = useRef({ kind: "idle" });
5472
5843
  useEffect(() => {
5473
5844
  const committedItems = items;
@@ -5485,6 +5856,16 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5485
5856
  transientItemsRef.current = null;
5486
5857
  setTransientItems(null);
5487
5858
  }, []);
5859
+ const clearNativeContextMenuLongPress = useCallback(() => {
5860
+ if (contextMenuLongPressTimerRef.current) {
5861
+ clearTimeout(contextMenuLongPressTimerRef.current);
5862
+ contextMenuLongPressTimerRef.current = null;
5863
+ }
5864
+ contextMenuLongPressStartRef.current = null;
5865
+ }, []);
5866
+ const closeNativeSelectionContextMenu = useCallback(() => {
5867
+ setSelectionContextMenu(null);
5868
+ }, []);
5488
5869
  const commitTransientItemsPreview = useCallback(() => {
5489
5870
  const nextItems = transientItemsRef.current;
5490
5871
  const change = onItemsChangeRef.current;
@@ -5517,6 +5898,9 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5517
5898
  if (laserClearTimerRef.current) {
5518
5899
  clearTimeout(laserClearTimerRef.current);
5519
5900
  }
5901
+ if (contextMenuLongPressTimerRef.current) {
5902
+ clearTimeout(contextMenuLongPressTimerRef.current);
5903
+ }
5520
5904
  },
5521
5905
  []
5522
5906
  );
@@ -5601,6 +5985,10 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5601
5985
  }
5602
5986
  const camera = cameraRef.current;
5603
5987
  const [cameraTick, setCameraTick] = useState(0);
5988
+ const selectedItems = useMemo(
5989
+ () => activeItems.filter((item) => selectedIds.includes(item.id)),
5990
+ [activeItems, selectedIds]
5991
+ );
5604
5992
  const screenToWorld = useCallback(
5605
5993
  (sx, sy) => {
5606
5994
  const cam = cameraRef.current;
@@ -5609,6 +5997,59 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5609
5997
  },
5610
5998
  []
5611
5999
  );
6000
+ const startNativeContextMenuLongPress = useCallback(
6001
+ (point) => {
6002
+ clearNativeContextMenuLongPress();
6003
+ if (toolIdRef.current !== "select") return;
6004
+ const { worldX, worldY } = screenToWorld(point.x, point.y);
6005
+ const request = resolveNativeSelectionContextMenuRequest({
6006
+ interactive,
6007
+ toolId: toolIdRef.current,
6008
+ selectedItems,
6009
+ worldPoint: { x: worldX, y: worldY },
6010
+ screenPoint: point
6011
+ });
6012
+ if (!request) return;
6013
+ contextMenuLongPressStartRef.current = point;
6014
+ contextMenuLongPressTimerRef.current = setTimeout(() => {
6015
+ contextMenuLongPressTimerRef.current = null;
6016
+ contextMenuLongPressStartRef.current = null;
6017
+ dragStateRef.current = { kind: "idle" };
6018
+ lastPanPoint.current = null;
6019
+ lastPinchDist.current = null;
6020
+ clearTransientItemsPreview();
6021
+ setRealtimePlacementPreview(null);
6022
+ setSelectionContextMenu(
6023
+ clampNativeContextMenuState(
6024
+ {
6025
+ ...request,
6026
+ canPaste: shapeClipboardRef.current !== null
6027
+ },
6028
+ size
6029
+ )
6030
+ );
6031
+ }, LONG_PRESS_CONTEXT_MENU_MS);
6032
+ },
6033
+ [
6034
+ clearNativeContextMenuLongPress,
6035
+ clearTransientItemsPreview,
6036
+ interactive,
6037
+ screenToWorld,
6038
+ selectedItems,
6039
+ setRealtimePlacementPreview,
6040
+ size
6041
+ ]
6042
+ );
6043
+ const cancelNativeContextMenuLongPressAfterMove = useCallback(
6044
+ (point) => {
6045
+ const start = contextMenuLongPressStartRef.current;
6046
+ if (!start) return;
6047
+ if (Math.hypot(point.x - start.x, point.y - start.y) > LONG_PRESS_CONTEXT_MENU_CANCEL_PX) {
6048
+ clearNativeContextMenuLongPress();
6049
+ }
6050
+ },
6051
+ [clearNativeContextMenuLongPress]
6052
+ );
5612
6053
  const notifyWorldPointerMove = useCallback(
5613
6054
  (point) => {
5614
6055
  const { worldX, worldY } = screenToWorld(point.x, point.y);
@@ -5631,10 +6072,6 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5631
6072
  }, []);
5632
6073
  const hideToolCursor = useCallback(() => {
5633
6074
  }, []);
5634
- const selectedItems = useMemo(
5635
- () => activeItems.filter((item) => selectedIds.includes(item.id)),
5636
- [activeItems, selectedIds]
5637
- );
5638
6075
  const selectedStyleInspectorState = useMemo(() => {
5639
6076
  const styleableItems = selectedItems.filter(
5640
6077
  (item) => !item.locked && getNativeStyleInspectorToolId(item) !== null
@@ -5679,10 +6116,77 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5679
6116
  },
5680
6117
  [patchCurrentStrokeStyle]
5681
6118
  );
6119
+ const copySelectedShapes = useCallback(() => {
6120
+ if (!interactive) return false;
6121
+ const clipboard = copyNativeSelectedShapeItems(
6122
+ itemsRef.current,
6123
+ selectedIdsRef.current
6124
+ );
6125
+ if (!clipboard) return false;
6126
+ shapeClipboardRef.current = clipboard;
6127
+ return true;
6128
+ }, [interactive]);
6129
+ const pasteCopiedShapes = useCallback(() => {
6130
+ if (!interactive) return false;
6131
+ const change = onItemsChangeRef.current;
6132
+ const clipboard = shapeClipboardRef.current;
6133
+ if (!change || !clipboard) return false;
6134
+ const pasted = pasteNativeShapeClipboard({
6135
+ items: itemsRef.current,
6136
+ clipboard
6137
+ });
6138
+ if (!pasted) return false;
6139
+ change(pasted.items);
6140
+ onSelectionChangeRef.current?.(pasted.selectedIds);
6141
+ return true;
6142
+ }, [interactive]);
6143
+ const duplicateSelectedShapes = useCallback(() => {
6144
+ if (!interactive) return false;
6145
+ const change = onItemsChangeRef.current;
6146
+ if (!change) return false;
6147
+ const duplicated = duplicateNativeSelectedShapes({
6148
+ items: itemsRef.current,
6149
+ selectedIds: selectedIdsRef.current
6150
+ });
6151
+ if (!duplicated) return false;
6152
+ change(duplicated.items);
6153
+ onSelectionChangeRef.current?.(duplicated.selectedIds);
6154
+ return true;
6155
+ }, [interactive]);
6156
+ const handleCopySelectedShapesFromMenu = useCallback(() => {
6157
+ copySelectedShapes();
6158
+ closeNativeSelectionContextMenu();
6159
+ }, [closeNativeSelectionContextMenu, copySelectedShapes]);
6160
+ const handlePasteCopiedShapesFromMenu = useCallback(() => {
6161
+ pasteCopiedShapes();
6162
+ closeNativeSelectionContextMenu();
6163
+ }, [closeNativeSelectionContextMenu, pasteCopiedShapes]);
6164
+ const handleDuplicateSelectedShapesFromMenu = useCallback(() => {
6165
+ duplicateSelectedShapes();
6166
+ closeNativeSelectionContextMenu();
6167
+ }, [closeNativeSelectionContextMenu, duplicateSelectedShapes]);
6168
+ const handleDeleteSelectedShapes = useCallback(() => {
6169
+ const menu = selectionContextMenuRef.current;
6170
+ const ids = menu?.itemIds ?? selectedIdsRef.current;
6171
+ if (ids.length === 0) return false;
6172
+ const change = onItemsChangeRef.current;
6173
+ if (!change) return false;
6174
+ const idSet = new Set(ids);
6175
+ const nextItems = itemsRef.current.filter((item) => !idSet.has(item.id));
6176
+ if (nextItems.length === itemsRef.current.length) return false;
6177
+ change(nextItems);
6178
+ onSelectionChangeRef.current?.(
6179
+ selectedIdsRef.current.filter((id) => !idSet.has(id))
6180
+ );
6181
+ closeNativeSelectionContextMenu();
6182
+ return true;
6183
+ }, [closeNativeSelectionContextMenu]);
5682
6184
  const lastPinchDist = useRef(null);
5683
6185
  const lastPanPoint = useRef(null);
5684
6186
  const beginDragAtScreenPoint = useCallback(
5685
6187
  (point) => {
6188
+ closeNativeSelectionContextMenu();
6189
+ startNativeContextMenuLongPress(point);
5686
6190
  lastPinchDist.current = null;
5687
6191
  lastPanPoint.current = null;
5688
6192
  const sx = point.x;
@@ -5896,15 +6400,18 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
5896
6400
  dragStateRef.current = { kind: "pan" };
5897
6401
  },
5898
6402
  [
6403
+ closeNativeSelectionContextMenu,
5899
6404
  interactive,
5900
6405
  requestSelectToolAfterUse,
5901
6406
  screenToWorld,
5902
6407
  setRealtimePlacementPreview,
6408
+ startNativeContextMenuLongPress,
5903
6409
  updateToolCursorPoint
5904
6410
  ]
5905
6411
  );
5906
6412
  const applyDragMoveAtScreenPoint = useCallback(
5907
6413
  (point, pagePoint) => {
6414
+ cancelNativeContextMenuLongPressAfterMove(point);
5908
6415
  const cam = cameraRef.current;
5909
6416
  if (!cam) return;
5910
6417
  updateToolCursorPoint(point);
@@ -6043,6 +6550,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
6043
6550
  }
6044
6551
  },
6045
6552
  [
6553
+ cancelNativeContextMenuLongPressAfterMove,
6046
6554
  requestRender,
6047
6555
  screenToWorld,
6048
6556
  setRealtimePlacementPreview,
@@ -6052,6 +6560,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
6052
6560
  );
6053
6561
  const finishDragAtScreenPoint = useCallback(
6054
6562
  (point) => {
6563
+ clearNativeContextMenuLongPress();
6055
6564
  lastPinchDist.current = null;
6056
6565
  lastPanPoint.current = null;
6057
6566
  updateToolCursorPoint(point);
@@ -6299,6 +6808,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
6299
6808
  dragStateRef.current = { kind: "idle" };
6300
6809
  },
6301
6810
  [
6811
+ clearNativeContextMenuLongPress,
6302
6812
  requestSelectToolAfterNativeLinkUse,
6303
6813
  requestSelectToolAfterUse,
6304
6814
  screenToWorld,
@@ -6342,6 +6852,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
6342
6852
  const sx = evt.nativeEvent.locationX;
6343
6853
  const sy = evt.nativeEvent.locationY;
6344
6854
  if (touches && touches.length >= 2) {
6855
+ clearNativeContextMenuLongPress();
6345
6856
  hideToolCursor();
6346
6857
  notifyWorldPointerLeave();
6347
6858
  dragStateRef.current = { kind: "pan" };
@@ -6358,6 +6869,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
6358
6869
  const pageX = evt.nativeEvent.pageX;
6359
6870
  const pageY = evt.nativeEvent.pageY;
6360
6871
  if (touches && touches.length >= 2) {
6872
+ clearNativeContextMenuLongPress();
6361
6873
  hideToolCursor();
6362
6874
  notifyWorldPointerLeave();
6363
6875
  const t0 = touches[0];
@@ -6388,6 +6900,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
6388
6900
  });
6389
6901
  },
6390
6902
  onPanResponderTerminate: () => {
6903
+ clearNativeContextMenuLongPress();
6391
6904
  lastPinchDist.current = null;
6392
6905
  lastPanPoint.current = null;
6393
6906
  hideToolCursor();
@@ -6404,6 +6917,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
6404
6917
  [
6405
6918
  applyDragMoveAtScreenPoint,
6406
6919
  beginDragAtScreenPoint,
6920
+ clearNativeContextMenuLongPress,
6407
6921
  finishDragAtScreenPoint,
6408
6922
  requestRender,
6409
6923
  hideToolCursor,
@@ -6429,9 +6943,18 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
6429
6943
  options?.padding ?? 0.08
6430
6944
  );
6431
6945
  requestRender();
6432
- }
6946
+ },
6947
+ copySelectedShapes,
6948
+ pasteCopiedShapes,
6949
+ duplicateSelectedShapes
6433
6950
  }),
6434
- [requestRender, size]
6951
+ [
6952
+ copySelectedShapes,
6953
+ duplicateSelectedShapes,
6954
+ pasteCopiedShapes,
6955
+ requestRender,
6956
+ size
6957
+ ]
6435
6958
  );
6436
6959
  const activeStyleToolId = toolId === "draw" || toolId === "marker" ? toolId : null;
6437
6960
  const styleInspectorToolId = selectedStyleInspectorState?.toolId ?? activeStyleToolId;
@@ -6549,7 +7072,17 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
6549
7072
  pointerEvents: "box-none",
6550
7073
  children: toolbar
6551
7074
  }
6552
- )
7075
+ ),
7076
+ interactive && selectionContextMenu ? /* @__PURE__ */ jsx(
7077
+ NativeSelectionContextMenu,
7078
+ {
7079
+ ...selectionContextMenu,
7080
+ onCopy: handleCopySelectedShapesFromMenu,
7081
+ onPaste: handlePasteCopiedShapesFromMenu,
7082
+ onDuplicate: handleDuplicateSelectedShapesFromMenu,
7083
+ onDelete: handleDeleteSelectedShapes
7084
+ }
7085
+ ) : null
6553
7086
  ] }),
6554
7087
  /* @__PURE__ */ jsx(
6555
7088
  Modal,
@@ -6612,6 +7145,50 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
6612
7145
  ] });
6613
7146
  });
6614
7147
  var styles4 = StyleSheet.create({
7148
+ nativeSelectionContextMenu: {
7149
+ position: "absolute",
7150
+ width: NATIVE_CONTEXT_MENU_WIDTH,
7151
+ minHeight: NATIVE_CONTEXT_MENU_HEIGHT,
7152
+ flexDirection: "column",
7153
+ alignItems: "stretch",
7154
+ justifyContent: "flex-start",
7155
+ padding: 4,
7156
+ borderRadius: 14,
7157
+ borderWidth: StyleSheet.hairlineWidth,
7158
+ borderColor: "#d4d4d8",
7159
+ backgroundColor: "#ffffff",
7160
+ shadowColor: "#000000",
7161
+ shadowOpacity: 0.16,
7162
+ shadowRadius: 18,
7163
+ shadowOffset: { width: 0, height: 8 },
7164
+ elevation: NATIVE_VIEWPORT_OVERLAY_ELEVATION + 4,
7165
+ zIndex: NATIVE_VIEWPORT_OVERLAY_Z_INDEX + 4
7166
+ },
7167
+ nativeSelectionContextMenuButton: {
7168
+ width: "100%",
7169
+ minHeight: 42,
7170
+ alignItems: "flex-start",
7171
+ justifyContent: "center",
7172
+ borderRadius: 10,
7173
+ paddingHorizontal: 14
7174
+ },
7175
+ nativeSelectionContextMenuButtonPressed: {
7176
+ backgroundColor: "#f4f4f5"
7177
+ },
7178
+ nativeSelectionContextMenuButtonDisabled: {
7179
+ opacity: 0.45
7180
+ },
7181
+ nativeSelectionContextMenuButtonText: {
7182
+ color: "#18181b",
7183
+ fontSize: 14,
7184
+ fontWeight: "400"
7185
+ },
7186
+ nativeSelectionContextMenuButtonTextDisabled: {
7187
+ color: "#71717a"
7188
+ },
7189
+ nativeSelectionContextMenuDeleteText: {
7190
+ color: "#dc2626"
7191
+ },
6615
7192
  nativeLinkDialogBackdrop: {
6616
7193
  flex: 1,
6617
7194
  alignItems: "center",