@mk-drag-and-drop/react 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@ callbacks, and optional announcements.
22
22
  The React package wraps `@mk-drag-and-drop/dom`.
23
23
 
24
24
  `DragProvider` creates and configures the DOM runtime through the package
25
- integration handle.
25
+ integration scope.
26
26
  Hooks such as `useDraggable`, `useDroppable`, `useSortable`, and
27
27
  `useDropContainer` register DOM nodes with that runtime through React refs.
28
28
  React hooks are adapters over DOM behavior, not a separate implementation.
@@ -52,11 +52,13 @@ The React root export includes:
52
52
  - `useSortable`, `UseSortableOptions`, `UseSortableResult`
53
53
  - `useDragHandle`, `UseDragHandleResult`
54
54
  - `useRemeasureDropTargets`
55
+ - `useRemeasureOverlay`
55
56
  - `composeRefs`
56
57
  - React-friendly `restrictToContainer`, `ReactRestrictToContainerInput`
57
58
  - DOM targeting helpers, modifier helpers, lifecycle/source/result types,
58
59
  sortable placement types, keyboard/pointer configuration types, and geometry
59
60
  types re-exported from the DOM package
61
+ - `OverlayReleaseMode`
60
62
  - `DragState` and `DragOverlayPhase` from the DOM integration layer
61
63
 
62
64
  ## Core Mental Model
@@ -72,11 +74,28 @@ The package reports drag/drop operations. Your app owns the data and the final
72
74
  UI commit. React state is one common way to commit the result, but it is not
73
75
  required by the package.
74
76
 
77
+ ## Lifetime Model
78
+
79
+ `DragProvider` is a drag-and-drop scope for a React subtree. It does not expose
80
+ runtime lifecycle controls, and application code does not dispose of the
81
+ runtime. The internal runtime is normal JavaScript memory and is released by
82
+ garbage collection when unreachable.
83
+
84
+ Hooks are adapters over the DOM behavior layer. They register committed DOM
85
+ nodes through refs/effects, update those registrations when ids, groups, or
86
+ container ids change, and unregister current nodes on ref/effect unmount.
87
+ Stale disconnected DOM registrations are still pruned by the shared DOM runtime
88
+ while the scope remains alive.
89
+
90
+ Pending pointer activation resources and active drag resources belong to the
91
+ input/drag lifecycle. Provider unmount releases active resources through the
92
+ same active-drag resource path used by DOM drag cancellation; it is not a
93
+ terminal runtime destructor. React StrictMode replay is supported for
94
+ draggable, droppable, sortable, overlay, callback, and announcement behavior.
95
+
75
96
  ## Installation
76
97
 
77
- This workspace currently marks `@mk-drag-and-drop/react` as private. Install the
78
- React and DOM packages from your workspace or package registry when they are
79
- available:
98
+ Install the React and DOM packages from your workspace or package registry:
80
99
 
81
100
  - `@mk-drag-and-drop/react`
82
101
  - `@mk-drag-and-drop/dom`
@@ -153,9 +172,11 @@ Real apps own the markup, layout, styling, state updates, and persistence.
153
172
 
154
173
  ## DragProvider
155
174
 
156
- `DragProvider` owns the runtime lifetime for the React subtree. It creates the
157
- DOM runtime, configures it from props, exposes it through React context, renders
158
- an optional drag overlay, and disposes the runtime when the provider unmounts.
175
+ `DragProvider` creates a DOM runtime scope for the React subtree, configures it
176
+ from props, exposes it through React context, and renders an optional drag
177
+ overlay. Provider effect cleanup is not a runtime destructor; runtime objects
178
+ are ordinary JavaScript objects and active drag resources are released by drag
179
+ drop/cancel paths.
159
180
 
160
181
  Key props:
161
182
 
@@ -171,8 +192,8 @@ Key props:
171
192
  - `pointerConfiguration`: configures pointer activation delay and distance.
172
193
  - `keyboardConfiguration`: enables and configures keyboard drag commands.
173
194
  - `announcements`: optional callbacks that return live-region messages.
174
- - `keepOverlayOnDrop`: keeps the overlay in the released phase until the overlay
175
- calls `finish`.
195
+ - `overlayRelease`: `"auto"` removes overlays on drag end; `"manual"` keeps a
196
+ released overlay until its `removeOverlay` callback is called.
176
197
 
177
198
  Callbacks receive operation information and helper methods from the runtime.
178
199
  Use those callbacks to commit app data. Do not mutate package internals.
@@ -202,6 +223,7 @@ type DragUpdateEvent = {
202
223
  draggableId: string;
203
224
  source: DragSource;
204
225
  pointerPosition: DragPoint;
226
+ overlayRect: DragRect | null;
205
227
  activeDropTargetId: string | null;
206
228
  previousDropTargetId: string | null;
207
229
  };
@@ -211,6 +233,7 @@ type DragEndEvent = {
211
233
  source: DragSource;
212
234
  result: DragEndResult;
213
235
  dropTargetId: string | null;
236
+ overlayRect: DragRect | null;
214
237
  };
215
238
 
216
239
  type DropEvent = {
@@ -221,6 +244,15 @@ type DropEvent = {
221
244
  };
222
245
  ```
223
246
 
247
+ `DragUpdateEvent.overlayRect` is the current viewport-space rectangle for the
248
+ drag overlay. `DragEndEvent.overlayRect` is the final viewport-space rectangle
249
+ at the moment the drag ends. React receives these DOM lifecycle event types
250
+ unchanged. The field is `null` when no drag overlay is configured. When an
251
+ overlay exists, it is based on cached overlay measurement plus modified pointer
252
+ movement, with the translated source rect as the fallback before overlay content
253
+ has been measured. React does not add overlay DOM measurement on every pointer
254
+ move.
255
+
224
256
  `onDrop` only runs for a valid successful drop. `onDragEnd.result` distinguishes
225
257
  successful drops, normal releases with no target, stale/invalid targets, and
226
258
  cancellations such as Escape or pointercancel. Plain drops are id-only.
@@ -365,9 +397,26 @@ requestAnimationFrame(() => {
365
397
 
366
398
  It accepts no argument, a target id, an array of target ids, or `{ group }`.
367
399
  Use it intentionally for expand/collapse, grouped trees, or drag-state layout
368
- changes. It is not needed for normal cleanup and should not run on every render.
369
- Sortable preview movement does not automatically remeasure a group; call this
370
- function when an app-owned layout change needs to affect targeting.
400
+ changes. It is not needed for normal resource release and should not run on
401
+ every render. Sortable preview movement does not automatically remeasure a
402
+ group; call this function when an app-owned layout change needs to affect
403
+ targeting.
404
+
405
+ ### useRemeasureOverlay
406
+
407
+ `useRemeasureOverlay` returns a function for manually refreshing the current
408
+ overlay measurement. It must be used inside `DragProvider`. Calling the returned
409
+ function with no mounted overlay is safe and does nothing.
410
+
411
+ ```tsx
412
+ const remeasureOverlay = useRemeasureOverlay();
413
+
414
+ remeasureOverlay();
415
+ ```
416
+
417
+ Normal overlay size changes are observed automatically with `ResizeObserver`
418
+ when available. Use this hook when the app knows visual geometry changed outside
419
+ that automatic path, such as after a CSS transform or transition.
371
420
 
372
421
  ## Sortable Behavior
373
422
 
@@ -408,8 +457,10 @@ shape.
408
457
 
409
458
  ```tsx
410
459
  <DragProvider
411
- dragOverlay={({ dragState }) => (
412
- <div className="dragOverlay">{dragState.draggableId}</div>
460
+ dragOverlay={({ dragState, remeasureOverlay }) => (
461
+ <div className="dragOverlay" onTransitionEnd={remeasureOverlay}>
462
+ {dragState.draggableId}
463
+ </div>
413
464
  )}
414
465
  >
415
466
  {children}
@@ -420,19 +471,37 @@ The overlay input includes:
420
471
 
421
472
  - `dragState`: item id, group, source rect, start pointer, and current pointer.
422
473
  - `phase`: `"dragging"` or `"released"`.
423
- - `finish`: call this when a kept release overlay should be removed.
474
+ - `remeasureOverlay`: manually refreshes the cached overlay measurement.
475
+ - `removeOverlay`: present only for `"released"` overlays in manual release
476
+ mode; call it when app-owned release animation should remove the overlay.
424
477
 
425
478
  Overlay rendering is app-owned. `DragProvider` mounts overlay content for the
426
- dragging phase and, when `keepOverlayOnDrop` is enabled, replaces it once for
427
- the released phase. Pointer movement updates the package-owned overlay host
428
- imperatively and does not call `dragOverlay` by default.
479
+ dragging phase. Default `overlayRelease: "auto"` removes the overlay
480
+ immediately on drag end. Manual release replaces it once for the released phase
481
+ and leaves removal to `removeOverlay`. Pointer movement updates the
482
+ package-owned overlay host imperatively and does not call `dragOverlay` by
483
+ default.
484
+
485
+ `removeOverlay` is idempotent and belongs to overlay release only. It is not a
486
+ provider, controller, or runtime teardown API.
429
487
 
430
- The package owns overlay hosting, movement, cleanup, and measurement for
488
+ The package owns overlay hosting, movement, release, and measurement for
431
489
  targeting and modifiers. It measures overlay content on mount/replacement and
432
- uses `ResizeObserver` when available to remeasure content size changes. Dynamic
433
- overlay content should use overlay-local state, component effects, lifecycle
434
- callbacks feeding a subscription/external store, or another app-owned update
435
- path rather than relying on `dragOverlay` being called for every pointer move.
490
+ uses `ResizeObserver` when available to remeasure content size changes. The
491
+ React package mirrors DOM behavior: overlay movement remains package-managed and
492
+ imperative, so pointer movement should not require React state updates on every
493
+ move.
494
+
495
+ Use the overlay input `remeasureOverlay` helper when overlay-local UI knows its
496
+ visual bounds changed. It uses the same provider/runtime path as
497
+ `useRemeasureOverlay` and avoids reaching into internal runtime or provider
498
+ state. CSS transforms can change visual bounds without necessarily triggering
499
+ `ResizeObserver`.
500
+
501
+ Dynamic overlay content should use overlay-local state, component effects,
502
+ lifecycle callbacks feeding a subscription/external store, or another app-owned
503
+ update path rather than relying on `dragOverlay` being called for every pointer
504
+ move.
436
505
 
437
506
  ## Targeting And Modifiers
438
507
 
@@ -451,7 +520,10 @@ The React package re-exports targeting helpers such as `pointerToCenter`,
451
520
  `maxPointerDistanceToRect` / `maxOverlayCenterDistanceToRect`, plus modifier
452
521
  helpers. Sortable placement remains separate from targeting: the configured
453
522
  targeting algorithm chooses the active target, and sortable only chooses
454
- before/after placement relative to that target.
523
+ before/after placement relative to that target. For sortable groups, the shared
524
+ DOM registry may narrow measured candidates to relevant sortable item, neighbor,
525
+ or container entries before the targeting algorithm runs; the algorithm still
526
+ chooses from that narrowed measured list.
455
527
 
456
528
  - `lockToXAxis()`
457
529
  - `lockToYAxis()`
@@ -507,19 +579,20 @@ Accessibility still depends on the app's markup and product behavior:
507
579
  - Do not assume this package alone provides complete screen-reader drag-and-drop
508
580
  support for your domain.
509
581
 
510
- ## Cleanup And Effects
582
+ ## Lifetime And Effects
511
583
 
512
584
  Hooks attach input props and register DOM nodes through refs and lifecycle
513
- effects. Users should not manually clean runtime entries for normal React
585
+ effects. Users should not manually release runtime entries for normal React
514
586
  mount/unmount.
515
587
 
516
- `DragProvider` owns runtime lifetime and disposes the runtime when it unmounts.
517
- `useDropContainer` also cleans its DOM binding on effect cleanup.
588
+ `DragProvider` does not call a terminal runtime destructor when it unmounts.
589
+ `useDropContainer` releases its current DOM registration from effect cleanup.
518
590
  `useDraggable`, `useDroppable`, and `useSortable` rely on React prop/ref updates
519
591
  for normal mount, unmount, and element replacement behavior.
520
592
 
521
- `useRemeasureDropTargets` is for layout changes, not cleanup. The runtime also
522
- prunes disconnected targets on drag-critical paths such as remeasurement.
593
+ `useRemeasureDropTargets` and `useRemeasureOverlay` are for geometry changes,
594
+ not resource release. The runtime also prunes disconnected targets on
595
+ drag-critical paths such as remeasurement.
523
596
 
524
597
  ## Examples
525
598
 
@@ -540,7 +613,7 @@ React examples live in `apps/react-web/src/react`:
540
613
  - `DragProvider`: creates/configures the DOM runtime for a React subtree.
541
614
  Important props include lifecycle callbacks, overlay rendering, targeting,
542
615
  constraints, modifiers, pointer/keyboard configuration, announcements, and
543
- `keepOverlayOnDrop`.
616
+ `overlayRelease`.
544
617
  - `useDraggable(options)`: registers a drag source. Important options:
545
618
  `draggableId`, `group`. Returns ref and input props.
546
619
  - `useDroppable(options)`: registers a drop target. Important options:
@@ -553,6 +626,8 @@ React examples live in `apps/react-web/src/react`:
553
626
  - `useDragHandle()`: returns the handle attribute props for a handle element.
554
627
  - `useRemeasureDropTargets()`: returns a function for remeasuring all targets,
555
628
  one target, multiple targets, or a group.
629
+ - `useRemeasureOverlay()`: returns a function for manually refreshing the
630
+ currently mounted overlay measurement.
556
631
  - `composeRefs`: helper for combining the refs returned by hooks with app refs.
557
632
  - `lockToXAxis`, `lockToYAxis`: DOM movement modifiers re-exported by React.
558
633
  - `restrictToContainer`: React-friendly container-bound modifier.
@@ -561,8 +636,9 @@ Important event/helper types are re-exported from the React package, including
561
636
  `DragStartEvent`, `DragUpdateEvent`, `DragEndEvent`, `DropEvent`,
562
637
  `DragSource`, `DragEndResult`, `DragLifecycleHelpers`, `DragState`,
563
638
  `DragOverlayPhase`, `SortableDropPlacement`, `RemeasureDropTargetsInput`,
564
- `PointerConfiguration`, `KeyboardConfiguration`, `KeyboardCommand`, `DropTarget`,
565
- targeting types, geometry types, and modifier types.
639
+ `OverlayReleaseMode`, `PointerConfiguration`, `KeyboardConfiguration`,
640
+ `KeyboardCommand`, `DropTarget`, targeting types, geometry types, and modifier
641
+ types.
566
642
 
567
643
  ## When To Use DOM Directly
568
644
 
@@ -577,7 +653,7 @@ Use `@mk-drag-and-drop/react` when:
577
653
 
578
654
  - Your UI is rendered with React.
579
655
  - You want ref-based hooks for registration.
580
- - You want provider-based runtime lifetime and lifecycle integration.
656
+ - You want provider-based runtime scoping and lifecycle integration.
581
657
 
582
658
  ## Development
583
659
 
@@ -1,6 +1,7 @@
1
- import type { DragRuntimeHandle } from "@mk-drag-and-drop/dom/integration";
1
+ import type { DragRuntimeScope } from "@mk-drag-and-drop/dom/integration";
2
2
  export type DragContextValue = {
3
- runtime: DragRuntimeHandle;
3
+ runtime: DragRuntimeScope;
4
4
  keyboardDragEnabled: boolean;
5
+ remeasureOverlay: () => void;
5
6
  };
6
7
  export declare const DragContext: import("react").Context<DragContextValue | null>;
@@ -3,11 +3,18 @@ import type { DragRect } from "@mk-drag-and-drop/dom";
3
3
  import type { DragOverlayPhase, DragState } from "@mk-drag-and-drop/dom/integration";
4
4
  export type DragOverlayInput = {
5
5
  dragState: DragState;
6
- phase: DragOverlayPhase;
7
- finish: () => void;
8
- };
6
+ } & ({
7
+ phase: Extract<DragOverlayPhase, "dragging">;
8
+ remeasureOverlay: () => void;
9
+ removeOverlay?: never;
10
+ } | {
11
+ phase: Extract<DragOverlayPhase, "released">;
12
+ remeasureOverlay: () => void;
13
+ removeOverlay: () => void;
14
+ });
9
15
  export type DragOverlayHostHandle = {
10
16
  move: (dragState: DragState) => void;
17
+ remeasure: () => void;
11
18
  };
12
19
  export declare const DragOverlayHost: import("react").NamedExoticComponent<{
13
20
  dragState: DragState;
@@ -1,7 +1,19 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { memo, useLayoutEffect, useRef, } from "react";
2
+ import { memo, useCallback, useLayoutEffect, useRef, } from "react";
3
3
  export const DragOverlayHost = memo(function DragOverlayHost({ dragState, children, contentId, onHostReady, onOverlayRectChange, }) {
4
4
  const overlayWrapperRef = useRef(null);
5
+ const measureOverlayElement = useCallback(() => {
6
+ const wrapper = overlayWrapperRef.current;
7
+ if (!wrapper) {
8
+ onOverlayRectChange?.(null);
9
+ return;
10
+ }
11
+ const measuredElement = getMeasuredOverlayElement(wrapper);
12
+ if (!measuredElement.isConnected) {
13
+ return;
14
+ }
15
+ onOverlayRectChange?.(domRectToDragRect(measuredElement.getBoundingClientRect()));
16
+ }, [onOverlayRectChange]);
5
17
  useLayoutEffect(() => {
6
18
  const wrapper = overlayWrapperRef.current;
7
19
  if (!wrapper) {
@@ -12,26 +24,21 @@ export const DragOverlayHost = memo(function DragOverlayHost({ dragState, childr
12
24
  move: (nextDragState) => {
13
25
  moveOverlayWrapper(wrapper, nextDragState);
14
26
  },
27
+ remeasure: measureOverlayElement,
15
28
  };
16
29
  onHostReady?.(host);
17
30
  host.move(dragState);
18
31
  return () => {
19
32
  onHostReady?.(null);
20
33
  };
21
- }, [dragState, onHostReady]);
34
+ }, [dragState, measureOverlayElement, onHostReady]);
22
35
  useLayoutEffect(() => {
23
36
  const wrapper = overlayWrapperRef.current;
24
37
  if (!wrapper) {
25
38
  onOverlayRectChange?.(null);
26
39
  return;
27
40
  }
28
- const measuredElement = wrapper.firstElementChild ?? wrapper;
29
- const measureOverlayElement = () => {
30
- if (!measuredElement.isConnected) {
31
- return;
32
- }
33
- onOverlayRectChange?.(domRectToDragRect(measuredElement.getBoundingClientRect()));
34
- };
41
+ const measuredElement = getMeasuredOverlayElement(wrapper);
35
42
  measureOverlayElement();
36
43
  if (typeof ResizeObserver === "undefined") {
37
44
  return () => {
@@ -46,7 +53,7 @@ export const DragOverlayHost = memo(function DragOverlayHost({ dragState, childr
46
53
  resizeObserver.disconnect();
47
54
  onOverlayRectChange?.(null);
48
55
  };
49
- }, [contentId, onOverlayRectChange]);
56
+ }, [contentId, measureOverlayElement, onOverlayRectChange]);
50
57
  return (_jsx("div", { ref: overlayWrapperRef, style: {
51
58
  position: "fixed",
52
59
  left: dragState.sourceRect.left,
@@ -58,6 +65,9 @@ export const DragOverlayHost = memo(function DragOverlayHost({ dragState, childr
58
65
  transform: getOverlayTransform(dragState),
59
66
  }, children: children }));
60
67
  });
68
+ function getMeasuredOverlayElement(wrapper) {
69
+ return wrapper.firstElementChild ?? wrapper;
70
+ }
61
71
  function moveOverlayWrapper(wrapper, dragState) {
62
72
  wrapper.style.transform = getOverlayTransform(dragState);
63
73
  }
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode } from "react";
2
- import { type DragEndEvent, type DragLifecycleCallbacks, type DragModifierInput, type DragStartEvent, type DragUpdateEvent, type DropEvent, type KeyboardConfiguration, type PointerConfiguration, type TargetingAlgorithm, type TargetingConstraint } from "@mk-drag-and-drop/dom";
2
+ import { type DragEndEvent, type DragLifecycleCallbacks, type DragModifierInput, type DragStartEvent, type DragUpdateEvent, type DropEvent, type KeyboardConfiguration, type OverlayReleaseMode, type PointerConfiguration, type TargetingAlgorithm, type TargetingConstraint } from "@mk-drag-and-drop/dom";
3
3
  import { type DragOverlayInput } from "./drag-overlay.js";
4
4
  export type DragAnnouncements = {
5
5
  onDragStart?: (event: DragStartEvent) => string | null;
@@ -12,10 +12,10 @@ export type DragProviderProps = {
12
12
  announcements?: DragAnnouncements;
13
13
  dragOverlay?: (overlay: DragOverlayInput) => ReactNode;
14
14
  keyboardConfiguration?: KeyboardConfiguration;
15
- keepOverlayOnDrop?: boolean;
16
15
  modifiers?: readonly DragModifierInput[];
16
+ overlayRelease?: OverlayReleaseMode;
17
17
  pointerConfiguration?: PointerConfiguration;
18
18
  targetingAlgorithm?: TargetingAlgorithm;
19
19
  targetingConstraint?: TargetingConstraint;
20
20
  } & DragLifecycleCallbacks;
21
- export declare function DragProvider({ children, announcements, dragOverlay, keyboardConfiguration, keepOverlayOnDrop, modifiers, pointerConfiguration, targetingAlgorithm, targetingConstraint, onDragStart, onDragUpdate, onDragEnd, onDrop, }: DragProviderProps): import("react").JSX.Element;
21
+ export declare function DragProvider({ children, announcements, dragOverlay, keyboardConfiguration, modifiers, overlayRelease, pointerConfiguration, targetingAlgorithm, targetingConstraint, onDragStart, onDragUpdate, onDragEnd, onDrop, }: DragProviderProps): import("react").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
3
3
  import { pointerToCenter, } from "@mk-drag-and-drop/dom";
4
- import { createDragRuntimeHandle, } from "@mk-drag-and-drop/dom/integration";
4
+ import { createDragRuntimeScope, } from "@mk-drag-and-drop/dom/integration";
5
5
  import { DragContext } from "./drag-context.js";
6
6
  import { DragOverlayHost, } from "./drag-overlay.js";
7
7
  const visuallyHiddenStyle = {
@@ -15,7 +15,7 @@ const visuallyHiddenStyle = {
15
15
  whiteSpace: "nowrap",
16
16
  border: 0,
17
17
  };
18
- export function DragProvider({ children, announcements, dragOverlay, keyboardConfiguration, keepOverlayOnDrop = false, modifiers, pointerConfiguration, targetingAlgorithm = pointerToCenter, targetingConstraint, onDragStart, onDragUpdate, onDragEnd, onDrop, }) {
18
+ export function DragProvider({ children, announcements, dragOverlay, keyboardConfiguration, modifiers, overlayRelease = "auto", pointerConfiguration, targetingAlgorithm = pointerToCenter, targetingConstraint, onDragStart, onDragUpdate, onDragEnd, onDrop, }) {
19
19
  const [overlayRequest, setOverlayRequest] = useState(null);
20
20
  const [announcementState, setAnnouncementState] = useState({
21
21
  id: 0,
@@ -38,7 +38,7 @@ export function DragProvider({ children, announcements, dragOverlay, keyboardCon
38
38
  const lifecycleCallbacksRef = useRef(null);
39
39
  const runtimeConfigurationSnapshotRef = useRef(null);
40
40
  const keyboardDragEnabled = isKeyboardDragEnabledFromConfiguration(keyboardConfiguration);
41
- const finishOverlay = useCallback(() => {
41
+ const clearOverlayHost = useCallback(() => {
42
42
  pendingOverlayDragStateRef.current = null;
43
43
  overlayHostRef.current = null;
44
44
  overlaySnapshotRef.current = null;
@@ -72,20 +72,29 @@ export function DragProvider({ children, announcements, dragOverlay, keyboardCon
72
72
  };
73
73
  }
74
74
  if (runtimeRef.current === null) {
75
- runtimeRef.current = createDragRuntimeHandle({
75
+ runtimeRef.current = createDragRuntimeScope({
76
76
  updateOverlayHost: handleOverlayHostUpdate,
77
77
  });
78
78
  }
79
79
  const runtime = runtimeRef.current;
80
+ const remeasureOverlay = useCallback(() => {
81
+ overlayHostRef.current?.remeasure();
82
+ }, []);
80
83
  const contextValue = useMemo(() => ({
81
84
  runtime,
82
85
  keyboardDragEnabled,
83
- }), [keyboardDragEnabled, runtime]);
86
+ remeasureOverlay,
87
+ }), [keyboardDragEnabled, remeasureOverlay, runtime]);
88
+ useEffect(() => {
89
+ return () => {
90
+ runtime.releaseActiveDragResources();
91
+ };
92
+ }, [runtime]);
84
93
  const nextRuntimeConfiguration = {
85
94
  targetingAlgorithm,
86
95
  targetingConstraint,
87
96
  hasDragOverlay: dragOverlay !== undefined,
88
- keepOverlayOnDrop,
97
+ overlayRelease,
89
98
  lifecycleCallbacks: lifecycleCallbacksRef.current,
90
99
  keyboardConfiguration,
91
100
  modifiers,
@@ -109,16 +118,11 @@ export function DragProvider({ children, announcements, dragOverlay, keyboardCon
109
118
  runtimeConfigurationSnapshotRef.current =
110
119
  createRuntimeConfigurationSnapshot(nextRuntimeConfiguration);
111
120
  });
112
- useEffect(() => {
113
- return () => {
114
- runtime.dispose();
115
- };
116
- }, [runtime]);
117
121
  useEffect(() => {
118
122
  if (!dragOverlay) {
119
- finishOverlay();
123
+ clearOverlayHost();
120
124
  }
121
- }, [dragOverlay, finishOverlay]);
125
+ }, [dragOverlay, clearOverlayHost]);
122
126
  useEffect(() => {
123
127
  if (!announcements) {
124
128
  lastAnnouncementRef.current = null;
@@ -154,13 +158,13 @@ export function DragProvider({ children, announcements, dragOverlay, keyboardCon
154
158
  moveOverlay(update.dragState);
155
159
  return;
156
160
  }
157
- finishOverlay();
161
+ clearOverlayHost();
158
162
  }
159
163
  function mountOverlay(state) {
160
164
  pendingOverlayDragStateRef.current = state.dragState;
161
165
  overlayHostRef.current?.move(state.dragState);
162
166
  if (!dragOverlayRef.current) {
163
- finishOverlay();
167
+ clearOverlayHost();
164
168
  return;
165
169
  }
166
170
  overlayContentIdRef.current += 1;
@@ -184,21 +188,32 @@ export function DragProvider({ children, announcements, dragOverlay, keyboardCon
184
188
  overlaySnapshotRef.current = {
185
189
  id: overlayRequest.id,
186
190
  state: overlayRequest.state,
187
- content: dragOverlay({
188
- dragState: overlayRequest.state.dragState,
189
- phase: overlayRequest.state.phase,
190
- finish: finishOverlay,
191
- }),
191
+ content: dragOverlay(createOverlayInput(overlayRequest.state)),
192
192
  };
193
193
  return overlaySnapshotRef.current;
194
194
  }
195
+ function createOverlayInput(overlayState) {
196
+ if (overlayState.phase === "released") {
197
+ return {
198
+ dragState: overlayState.dragState,
199
+ phase: "released",
200
+ remeasureOverlay,
201
+ removeOverlay: clearOverlayHost,
202
+ };
203
+ }
204
+ return {
205
+ dragState: overlayState.dragState,
206
+ phase: "dragging",
207
+ remeasureOverlay,
208
+ };
209
+ }
195
210
  }
196
211
  function createRuntimeConfigurationSnapshot(input) {
197
212
  return {
198
213
  targetingAlgorithm: input.targetingAlgorithm,
199
214
  targetingConstraint: input.targetingConstraint,
200
215
  hasDragOverlay: input.hasDragOverlay,
201
- keepOverlayOnDrop: input.keepOverlayOnDrop,
216
+ overlayRelease: input.overlayRelease,
202
217
  keyboardConfiguration: cloneKeyboardConfiguration(input.keyboardConfiguration),
203
218
  modifiers: input.modifiers ? Array.from(input.modifiers) : undefined,
204
219
  pointerConfiguration: input.pointerConfiguration
@@ -211,7 +226,7 @@ function areRuntimeConfigurationSnapshotsEqual(previous, next) {
211
226
  previous.targetingAlgorithm === next.targetingAlgorithm &&
212
227
  previous.targetingConstraint === next.targetingConstraint &&
213
228
  previous.hasDragOverlay === next.hasDragOverlay &&
214
- previous.keepOverlayOnDrop === next.keepOverlayOnDrop &&
229
+ previous.overlayRelease === next.overlayRelease &&
215
230
  areKeyboardConfigurationsEqual(previous.keyboardConfiguration, next.keyboardConfiguration) &&
216
231
  arePointerConfigurationsEqual(previous.pointerConfiguration, next.pointerConfiguration) &&
217
232
  areModifierInputsEqual(previous.modifiers, next.modifiers));
@@ -19,7 +19,7 @@ export function useDropContainer({ containerId, group = defaultDropContainerGrou
19
19
  existingBehavior.group === group) {
20
20
  return existingBehavior.behavior;
21
21
  }
22
- existingBehavior?.behavior.cleanup();
22
+ existingBehavior?.behavior.releaseRegistration();
23
23
  const behavior = createDomDropContainer({
24
24
  runtime,
25
25
  containerId,
@@ -44,7 +44,7 @@ export function useDropContainer({ containerId, group = defaultDropContainerGrou
44
44
  behavior.setElement(nodeRef.current);
45
45
  }
46
46
  return () => {
47
- behavior.cleanup();
47
+ behavior.releaseRegistration();
48
48
  if (behaviorRef.current?.behavior === behavior) {
49
49
  behaviorRef.current = null;
50
50
  }
@@ -1,9 +1,10 @@
1
- import { useCallback, useContext, useMemo, } from "react";
1
+ import { useCallback, useContext, useEffect, useMemo, useRef, } from "react";
2
2
  import { createDomDroppable } from "@mk-drag-and-drop/dom/integration";
3
3
  import { DragContext } from "../drag-context.js";
4
4
  const defaultDroppableGroup = "default";
5
5
  export function useDroppable({ dropTargetId, group = defaultDroppableGroup, containerId = null, }) {
6
6
  const context = useContext(DragContext);
7
+ const nodeRef = useRef(null);
7
8
  if (!context) {
8
9
  throw new Error("useDroppable must be used inside DragProvider");
9
10
  }
@@ -15,8 +16,17 @@ export function useDroppable({ dropTargetId, group = defaultDroppableGroup, cont
15
16
  containerId,
16
17
  }), [containerId, dropTargetId, group, runtime]);
17
18
  const setNodeRef = useCallback((node) => {
19
+ nodeRef.current = node;
18
20
  behavior.setElement(node);
19
21
  }, [behavior]);
22
+ useEffect(() => {
23
+ if (nodeRef.current) {
24
+ behavior.setElement(nodeRef.current);
25
+ }
26
+ return () => {
27
+ behavior.releaseRegistration();
28
+ };
29
+ }, [behavior]);
20
30
  return useMemo(() => ({
21
31
  ref: setNodeRef,
22
32
  }), [setNodeRef]);
@@ -0,0 +1 @@
1
+ export declare function useRemeasureOverlay(): () => void;
@@ -0,0 +1,12 @@
1
+ import { useCallback, useContext } from "react";
2
+ import { DragContext } from "../drag-context.js";
3
+ export function useRemeasureOverlay() {
4
+ const context = useContext(DragContext);
5
+ if (!context) {
6
+ throw new Error("useRemeasureOverlay must be used inside DragProvider");
7
+ }
8
+ const { remeasureOverlay } = context;
9
+ return useCallback(() => {
10
+ remeasureOverlay();
11
+ }, [remeasureOverlay]);
12
+ }
@@ -1,4 +1,4 @@
1
- import { useCallback, useContext, useMemo, useRef, } from "react";
1
+ import { useCallback, useContext, useEffect, useMemo, useRef, } from "react";
2
2
  import { createDomSortable, } from "@mk-drag-and-drop/dom/integration";
3
3
  import { DragContext } from "../drag-context.js";
4
4
  const defaultSortableGroup = "default";
@@ -33,6 +33,14 @@ export function useSortable({ draggableId, group = defaultSortableGroup, contain
33
33
  nodeRef.current = node;
34
34
  behavior.setElement(node);
35
35
  }, [behavior]);
36
+ useEffect(() => {
37
+ if (nodeRef.current) {
38
+ behavior.setElement(nodeRef.current);
39
+ }
40
+ return () => {
41
+ behavior.releaseRegistration();
42
+ };
43
+ }, [behavior]);
36
44
  return useMemo(() => {
37
45
  const sortableProps = {
38
46
  ref: setNodeRef,
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { DragProvider, type DragAnnouncements, type DragProviderProps, } from "./drag-provider.js";
2
2
  export type { DragOverlayInput } from "./drag-overlay.js";
3
- export { centerToCenter, getDistanceToRect, lockToXAxis, lockToYAxis, maxOverlayCenterDistanceToRect, maxPointerDistanceToRect, pointerToCenter, pointerToRectDistance, type DragEndResult, type DragEndEvent, type DragLifecycleCallbacks, type DragLifecycleHelpers, type DragModifier, type DragModifierInput, type DragModifierSetupInput, type DragModifierTransformInput, type DragPoint, type DragRect, type DragSource, type DragStartEvent, type DragUpdateEvent, type DropEvent, type DropTarget, type KeyboardCommand, type KeyboardConfiguration, type PointerConfiguration, type RemeasureDropTargetsInput, type SortableAxis, type SortableDropPlacement, type SortablePlacementBoundary, type TargetingAlgorithm, type TargetingAlgorithmInput, type TargetingConstraint, type TargetingConstraintInput, } from "@mk-drag-and-drop/dom";
3
+ export { centerToCenter, getDistanceToRect, lockToXAxis, lockToYAxis, maxOverlayCenterDistanceToRect, maxPointerDistanceToRect, pointerToCenter, pointerToRectDistance, type DragEndResult, type DragEndEvent, type DragLifecycleCallbacks, type DragLifecycleHelpers, type DragModifier, type DragModifierInput, type DragModifierSetupInput, type DragModifierTransformInput, type DragPoint, type DragRect, type DragSource, type DragStartEvent, type DragUpdateEvent, type DropEvent, type DropTarget, type KeyboardCommand, type KeyboardConfiguration, type OverlayReleaseMode, type PointerConfiguration, type RemeasureDropTargetsInput, type SortableAxis, type SortableDropPlacement, type SortablePlacementBoundary, type TargetingAlgorithm, type TargetingAlgorithmInput, type TargetingConstraint, type TargetingConstraintInput, } from "@mk-drag-and-drop/dom";
4
4
  export { restrictToContainer, type ReactRestrictToContainerInput, } from "./modifiers/restrict-to-container.js";
5
5
  export type { DragOverlayPhase, DragState, } from "@mk-drag-and-drop/dom/integration";
6
6
  export { useDragHandle, type UseDragHandleResult, } from "./hooks/use-drag-handle.js";
@@ -8,5 +8,6 @@ export { useDraggable, type UseDraggableOptions, type UseDraggableResult, } from
8
8
  export { useDropContainer, type UseDropContainerOptions, type UseDropContainerResult, } from "./hooks/use-drop-container.js";
9
9
  export { useDroppable, type UseDroppableOptions, type UseDroppableResult, } from "./hooks/use-droppable.js";
10
10
  export { useRemeasureDropTargets } from "./hooks/use-remeasure-drop-targets.js";
11
+ export { useRemeasureOverlay } from "./hooks/use-remeasure-overlay.js";
11
12
  export { useSortable, type UseSortableOptions, type UseSortableResult, } from "./hooks/use-sortable.js";
12
13
  export { composeRefs } from "./utils/compose-refs.js";
package/dist/index.js CHANGED
@@ -6,5 +6,6 @@ export { useDraggable, } from "./hooks/use-draggable.js";
6
6
  export { useDropContainer, } from "./hooks/use-drop-container.js";
7
7
  export { useDroppable, } from "./hooks/use-droppable.js";
8
8
  export { useRemeasureDropTargets } from "./hooks/use-remeasure-drop-targets.js";
9
+ export { useRemeasureOverlay } from "./hooks/use-remeasure-overlay.js";
9
10
  export { useSortable, } from "./hooks/use-sortable.js";
10
11
  export { composeRefs } from "./utils/compose-refs.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mk-drag-and-drop/react",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "React hooks and provider for the mk drag-and-drop DOM runtime.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://mkramer.dev",
@@ -37,8 +37,9 @@
37
37
  "files": [
38
38
  "dist"
39
39
  ],
40
+ "sideEffects": false,
40
41
  "dependencies": {
41
- "@mk-drag-and-drop/dom": "0.1.0"
42
+ "@mk-drag-and-drop/dom": "^0.3.0"
42
43
  },
43
44
  "peerDependencies": {
44
45
  "react": "^19.0.0"