ngx-virtual-dnd 3.2.0-alpha.1 → 3.2.0-alpha.2

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.
@@ -456,12 +456,17 @@ const VDND_VIRTUAL_VIEWPORT = new InjectionToken('VDND_VIRTUAL_VIEWPORT');
456
456
  const DEFAULT_SHIFT_DURATION = 200;
457
457
  /** Default shift animation easing used when the config omits `shiftEasing`. */
458
458
  const DEFAULT_SHIFT_EASING = 'cubic-bezier(0.2, 0, 0, 1)';
459
+ /** Default drop animation duration (ms) used when the config omits `dropDuration`. */
460
+ const DEFAULT_DROP_DURATION = 200;
461
+ /** Default drop animation easing used when the config omits `dropEasing`. */
462
+ const DEFAULT_DROP_EASING = 'cubic-bezier(0.2, 0, 0, 1)';
459
463
  /**
460
464
  * Opt-in animation configuration. When provided, items displaced by the placeholder
461
- * slide into their new position instead of jumping. Resolved through the element
465
+ * slide into their new position instead of jumping, and on drop the drag preview glides
466
+ * into the item's final position. Resolved through the element
462
467
  * injector, so it can be provided app-wide or per component subtree.
463
468
  *
464
- * The animation is skipped when the user prefers reduced motion. Values are read each
469
+ * Animations are skipped when the user prefers reduced motion. Values are read each
465
470
  * time an animation starts, so getters can toggle the animation at runtime.
466
471
  *
467
472
  * @example
@@ -1235,9 +1240,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
1235
1240
  * AutoScrollService registers here to perform edge-scroll + immediate
1236
1241
  * placeholder recalculation in the same synchronous frame — preserving
1237
1242
  * the Safari synchronous-callback constraint.
1238
- * 2. Main compute phase (COMPUTE+WRITE): onTick is called with the latest
1239
- * cursor. Only runs when the cursor has changed (cursorDirty=true), so
1240
- * autoscroll-only frames (cursor stationary) incur zero hit-test work.
1243
+ * 2. Main compute phase (COMPUTE+WRITE): onTick is called every frame with the
1244
+ * latest cursor and a `cursorDirty` flag. Callers skip their hit-test when
1245
+ * the flag is false, so autoscroll-only frames (cursor stationary) incur
1246
+ * zero hit-test work.
1241
1247
  *
1242
1248
  * PointerDragHandler calls `queueCursorUpdate()` instead of managing its own
1243
1249
  * RAF per pointermove. This coalesces all moves within a frame into one update
@@ -2488,7 +2494,7 @@ const MIN_SHIFT_PX = 0.5;
2488
2494
  * FLIP animator for items displaced by the drag placeholder.
2489
2495
  *
2490
2496
  * Call `beforeUpdate()` before the DOM reflects a new drag/placeholder state. When the
2491
- * placeholder moved, it snapshots every rendered element's visual position, then after
2497
+ * placeholder moved (or the drag ended), it snapshots every rendered element's visual position, then after
2492
2498
  * the render measures again and plays a compositor-only `transform` animation from the
2493
2499
  * old position to the new one.
2494
2500
  *
@@ -2525,11 +2531,19 @@ class ShiftAnimator {
2525
2531
  this.#wasDragging = isDragging;
2526
2532
  this.#lastPlaceholderIndex = placeholderIndex;
2527
2533
  if (!isDragging) {
2528
- // Drop/cancel: the list re-renders with the committed order, so snap everything.
2529
- this.#snapshot = null;
2530
- if (wasDragging) {
2534
+ // Drop/cancel: one last pass slides rows from their drag layout into the committed
2535
+ // order. After a drop the rows usually already sit in their final slots, so in-flight
2536
+ // slides simply finish; after a cancel the displaced rows slide back.
2537
+ // A pending snapshot already holds this tick's "before".
2538
+ if (!wasDragging || this.#snapshot) {
2539
+ return;
2540
+ }
2541
+ if (!this.#isEnabled()) {
2531
2542
  this.cancelAll();
2543
+ return;
2532
2544
  }
2545
+ this.#snapshot = this.#measure();
2546
+ afterNextRender(() => this.#play(), { injector: this.#injector });
2533
2547
  return;
2534
2548
  }
2535
2549
  // Drag start renders the placeholder in the hidden item's slot — nothing moves.
@@ -2599,7 +2613,7 @@ class ShiftAnimator {
2599
2613
  #play() {
2600
2614
  const first = this.#snapshot;
2601
2615
  this.#snapshot = null;
2602
- if (!first || !this.#wasDragging)
2616
+ if (!first)
2603
2617
  return;
2604
2618
  const last = this.#measure();
2605
2619
  const stale = [];
@@ -3352,6 +3366,124 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
3352
3366
  `, styles: [":host{display:block;overflow:auto;position:relative;overflow-anchor:none}.vdnd-virtual-scroll-spacer{position:absolute;top:0;left:0;width:1px;visibility:hidden;pointer-events:none}.vdnd-virtual-scroll-content-wrapper{position:absolute;top:0;left:0;right:0;will-change:transform}\n"] }]
3353
3367
  }], ctorParameters: () => [], propDecorators: { itemTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: true }] }], scrollContainerId: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollContainerId", required: false }] }], autoScrollEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoScrollEnabled", required: false }] }], autoScrollConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoScrollConfig", required: false }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], itemHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemHeight", required: true }] }], dynamicItemHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "dynamicItemHeight", required: false }] }], containerHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "containerHeight", required: false }] }], overscan: [{ type: i0.Input, args: [{ isSignal: true, alias: "overscan", required: false }] }], stickyItemIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "stickyItemIds", required: false }] }], itemIdFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemIdFn", required: true }] }], trackByFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackByFn", required: false }] }], droppableId: [{ type: i0.Input, args: [{ isSignal: true, alias: "droppableId", required: false }] }], autoStickyDraggedItem: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoStickyDraggedItem", required: false }] }] } });
3354
3368
 
3369
+ /**
3370
+ * Plays the drop ("settle") animation: the drag preview glides from where it was released
3371
+ * into the dropped item's final position, while that item stays invisible underneath.
3372
+ *
3373
+ * Purely visual. The drag state and the drop events are already final when it starts, so it
3374
+ * never delays or changes a drop. Without a target (the item is not rendered, or scrolled out
3375
+ * of view) the ghost fades out where it was released.
3376
+ * @internal
3377
+ */
3378
+ class DropAnimator {
3379
+ #config;
3380
+ #ghostAnimation = null;
3381
+ #hideAnimation = null;
3382
+ constructor(config) {
3383
+ this.#config = config;
3384
+ }
3385
+ /** Whether a drop animation would play right now (duration, WAAPI, reduced motion). */
3386
+ isEnabled() {
3387
+ if (this.#duration() <= 0)
3388
+ return false;
3389
+ if (typeof Element === 'undefined' || typeof Element.prototype.animate !== 'function') {
3390
+ return false;
3391
+ }
3392
+ if (typeof matchMedia !== 'function')
3393
+ return true;
3394
+ return !matchMedia('(prefers-reduced-motion: reduce)').matches;
3395
+ }
3396
+ /**
3397
+ * Animate `ghost` onto `target` (or fade it out without one), then call `onDone`.
3398
+ * `onDone` is not called when the animation is cancelled.
3399
+ */
3400
+ play(ghost, target, onDone) {
3401
+ this.cancel();
3402
+ const timing = {
3403
+ duration: this.#duration(),
3404
+ easing: this.#config.dropEasing ?? DEFAULT_DROP_EASING,
3405
+ fill: 'forwards',
3406
+ };
3407
+ let ghostAnimation;
3408
+ if (target) {
3409
+ const from = ghost.getBoundingClientRect();
3410
+ const dx = target.rect.left - from.left;
3411
+ const dy = target.rect.top - from.top;
3412
+ // Travel on top of the preview's own position transform (width/height must not be
3413
+ // composited, so the base transform is repeated instead of using composite: 'add').
3414
+ const base = ghost.style.transform;
3415
+ ghostAnimation = ghost.animate([
3416
+ {
3417
+ transform: `${base} translate(0px, 0px)`,
3418
+ width: `${from.width}px`,
3419
+ height: `${from.height}px`,
3420
+ },
3421
+ {
3422
+ transform: `${base} translate(${dx}px, ${dy}px)`,
3423
+ width: `${target.rect.width}px`,
3424
+ height: `${target.rect.height}px`,
3425
+ },
3426
+ ], timing);
3427
+ // Keep the real item invisible until the ghost lands on it, so it is never seen twice.
3428
+ this.#hideAnimation = target.element.animate([{ opacity: 0 }, { opacity: 0 }], {
3429
+ duration: timing.duration,
3430
+ });
3431
+ }
3432
+ else {
3433
+ ghostAnimation = ghost.animate([{ opacity: 1 }, { opacity: 0 }], timing);
3434
+ }
3435
+ this.#ghostAnimation = ghostAnimation;
3436
+ ghostAnimation.onfinish = () => {
3437
+ if (this.#ghostAnimation !== ghostAnimation)
3438
+ return;
3439
+ this.cancel();
3440
+ onDone();
3441
+ };
3442
+ }
3443
+ /** Stop the running animation immediately and reveal the real item. */
3444
+ cancel() {
3445
+ const ghostAnimation = this.#ghostAnimation;
3446
+ const hideAnimation = this.#hideAnimation;
3447
+ this.#ghostAnimation = null;
3448
+ this.#hideAnimation = null;
3449
+ ghostAnimation?.cancel();
3450
+ hideAnimation?.cancel();
3451
+ }
3452
+ #duration() {
3453
+ return this.#config.dropDuration ?? DEFAULT_DROP_DURATION;
3454
+ }
3455
+ }
3456
+ /**
3457
+ * Find the rendered element of a dropped item: first in the droppable it was dropped on, then
3458
+ * in the one it came from (a drop the consumer rejected or has not committed yet leaves it
3459
+ * there). Returns null when it is not rendered, or rendered but scrolled out of its list's view.
3460
+ */
3461
+ function findDropTarget(draggableId, droppableIds) {
3462
+ if (typeof document === 'undefined')
3463
+ return null;
3464
+ for (const droppableId of droppableIds) {
3465
+ if (!droppableId)
3466
+ continue;
3467
+ const droppable = queryByAttribute(document, 'data-droppable-id', droppableId);
3468
+ const element = droppable
3469
+ ? queryByAttribute(droppable, 'data-draggable-id', draggableId)
3470
+ : null;
3471
+ if (!droppable || !element)
3472
+ continue;
3473
+ const rect = element.getBoundingClientRect();
3474
+ // Hidden, or virtualized/scrolled out of the list's visible box: nowhere to land.
3475
+ if (rect.width === 0 && rect.height === 0)
3476
+ return null;
3477
+ const bounds = droppable.getBoundingClientRect();
3478
+ const visible = rect.bottom > bounds.top &&
3479
+ rect.top < bounds.bottom &&
3480
+ rect.right > bounds.left &&
3481
+ rect.left < bounds.right;
3482
+ return visible ? { element, rect } : null;
3483
+ }
3484
+ return null;
3485
+ }
3486
+
3355
3487
  /**
3356
3488
  * Renders a preview of the dragged item that follows the cursor.
3357
3489
  *
@@ -3359,6 +3491,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
3359
3491
  * so it works correctly even inside ancestors with CSS `transform` (e.g. Ionic pages).
3360
3492
  * It can be placed anywhere in the component tree.
3361
3493
  *
3494
+ * When `VDND_ANIMATION_CONFIG` is provided, the preview stays up briefly after a drop or
3495
+ * cancel and glides into the item's final position.
3496
+ *
3362
3497
  * @example
3363
3498
  * ```html
3364
3499
  * <vdnd-drag-preview>
@@ -3372,12 +3507,24 @@ class DragPreviewComponent {
3372
3507
  dragState = inject(DragStateService);
3373
3508
  #overlayContainer = inject(OverlayContainerService);
3374
3509
  #elementRef = inject((ElementRef));
3510
+ #injector = inject(Injector);
3511
+ #dropAnimator = this.#createDropAnimator();
3375
3512
  /** Optional custom template for the preview */
3376
3513
  previewTemplate = input(...(ngDevMode ? [undefined, { debugName: "previewTemplate" }] : []));
3377
3514
  /** Offset from cursor to preview (to avoid cursor being on top of preview) */
3378
3515
  cursorOffset = input({ x: 8, y: 8 }, ...(ngDevMode ? [{ debugName: "cursorOffset" }] : []));
3379
3516
  /** Reference to the clone container element (cannot use ES private with viewChild) */
3380
3517
  cloneContainer = viewChild('cloneContainer', ...(ngDevMode ? [{ debugName: "cloneContainer" }] : []));
3518
+ /** The rendered preview box (cannot use ES private with viewChild) */
3519
+ previewElement = viewChild('preview', ...(ngDevMode ? [{ debugName: "previewElement" }] : []));
3520
+ /** Set while the preview animates into the drop position after the drag ended */
3521
+ #settling = signal(null, ...(ngDevMode ? [{ debugName: "#settling" }] : []));
3522
+ /** Whether the drag was still active on the last state change (detects drag end) */
3523
+ #wasDragging = false;
3524
+ /** Whether the preview is playing the drop animation */
3525
+ isSettling = computed(() => this.#settling() !== null, ...(ngDevMode ? [{ debugName: "isSettling" }] : []));
3526
+ /** The item shown: the live dragged item, or the dropped one while it settles */
3527
+ displayedItem = computed(() => this.dragState.draggedItem() ?? this.#settling()?.item ?? null, ...(ngDevMode ? [{ debugName: "displayedItem" }] : []));
3381
3528
  /** Whether this preview's template registration is currently counted (kept balanced). */
3382
3529
  #templateRegistered = false;
3383
3530
  /** The cloned element from drag state (used when no custom template is provided) */
@@ -3385,7 +3532,7 @@ class DragPreviewComponent {
3385
3532
  if (this.previewTemplate()) {
3386
3533
  return null; // Custom template takes precedence
3387
3534
  }
3388
- return this.dragState.draggedItem()?.clonedElement ?? null;
3535
+ return this.displayedItem()?.clonedElement ?? null;
3389
3536
  }, ...(ngDevMode ? [{ debugName: "clonedElement" }] : []));
3390
3537
  constructor() {
3391
3538
  // Teleport host element into the body-level overlay container after first render.
@@ -3423,8 +3570,24 @@ class DragPreviewComponent {
3423
3570
  container.appendChild(clone);
3424
3571
  }
3425
3572
  });
3573
+ // Drop animation: when a drag ends, keep the preview up and glide it into place.
3574
+ // A new drag cuts any settle short.
3575
+ effect(() => {
3576
+ const isDragging = this.dragState.isDragging();
3577
+ untracked(() => {
3578
+ const wasDragging = this.#wasDragging;
3579
+ this.#wasDragging = isDragging;
3580
+ if (isDragging) {
3581
+ this.#stopSettling();
3582
+ }
3583
+ else if (wasDragging) {
3584
+ this.#startSettling();
3585
+ }
3586
+ });
3587
+ });
3426
3588
  }
3427
3589
  ngOnDestroy() {
3590
+ this.#dropAnimator?.cancel();
3428
3591
  if (this.#templateRegistered) {
3429
3592
  this.#overlayContainer.setTemplatePreviewActive(false);
3430
3593
  this.#templateRegistered = false;
@@ -3433,20 +3596,30 @@ class DragPreviewComponent {
3433
3596
  }
3434
3597
  /** Whether the preview is visible */
3435
3598
  isVisible = computed(() => {
3436
- return this.dragState.isDragging() && this.dragState.cursorPosition() !== null;
3599
+ return ((this.dragState.isDragging() && this.dragState.cursorPosition() !== null) || this.isSettling());
3437
3600
  }, ...(ngDevMode ? [{ debugName: "isVisible" }] : []));
3438
3601
  /** Position of the preview */
3439
3602
  position = computed(() => {
3440
- const cursor = this.dragState.cursorPosition();
3441
- const grabOffset = this.dragState.grabOffset();
3442
- const fallbackOffset = this.cursorOffset();
3443
- const initialPosition = this.dragState.initialPosition();
3444
- const lockAxis = this.dragState.lockAxis();
3603
+ const settling = this.#settling();
3604
+ if (settling && !this.dragState.isDragging()) {
3605
+ return settling.position;
3606
+ }
3607
+ return this.#positionFor({
3608
+ cursorPosition: this.dragState.cursorPosition(),
3609
+ grabOffset: this.dragState.grabOffset(),
3610
+ initialPosition: this.dragState.initialPosition(),
3611
+ lockAxis: this.dragState.lockAxis(),
3612
+ });
3613
+ }, ...(ngDevMode ? [{ debugName: "position" }] : []));
3614
+ #positionFor(state) {
3615
+ const cursor = state.cursorPosition;
3616
+ const initialPosition = state.initialPosition;
3617
+ const lockAxis = state.lockAxis;
3445
3618
  if (!cursor) {
3446
3619
  return { x: 0, y: 0 };
3447
3620
  }
3448
3621
  // Use grab offset if available (preserves grab position), otherwise fall back to cursorOffset input
3449
- const offset = grabOffset ?? fallbackOffset;
3622
+ const offset = state.grabOffset ?? this.cursorOffset();
3450
3623
  let x = cursor.x - offset.x;
3451
3624
  let y = cursor.y - offset.y;
3452
3625
  // Apply axis locking if configured
@@ -3461,7 +3634,7 @@ class DragPreviewComponent {
3461
3634
  }
3462
3635
  }
3463
3636
  return { x, y };
3464
- }, ...(ngDevMode ? [{ debugName: "position" }] : []));
3637
+ }
3465
3638
  /** Transform-based positioning for better performance (avoid layout from left/top). */
3466
3639
  transform = computed(() => {
3467
3640
  const { x, y } = this.position();
@@ -3469,7 +3642,7 @@ class DragPreviewComponent {
3469
3642
  }, ...(ngDevMode ? [{ debugName: "transform" }] : []));
3470
3643
  /** Dimensions of the preview */
3471
3644
  dimensions = computed(() => {
3472
- const item = this.dragState.draggedItem();
3645
+ const item = this.displayedItem();
3473
3646
  if (!item) {
3474
3647
  return { width: 100, height: 50 };
3475
3648
  }
@@ -3480,19 +3653,65 @@ class DragPreviewComponent {
3480
3653
  }, ...(ngDevMode ? [{ debugName: "dimensions" }] : []));
3481
3654
  /** Template context */
3482
3655
  templateContext = computed(() => {
3483
- const item = this.dragState.draggedItem();
3656
+ const item = this.displayedItem();
3484
3657
  return {
3485
3658
  $implicit: (item?.data ?? null),
3486
3659
  draggableId: item?.draggableId ?? '',
3487
3660
  droppableId: item?.droppableId ?? '',
3488
3661
  };
3489
3662
  }, ...(ngDevMode ? [{ debugName: "templateContext" }] : []));
3663
+ #createDropAnimator() {
3664
+ const config = inject(VDND_ANIMATION_CONFIG, { optional: true });
3665
+ return config ? new DropAnimator(config) : null;
3666
+ }
3667
+ /**
3668
+ * Keep the ended drag's preview on screen, then (once the consumer's drop handler has
3669
+ * re-rendered the lists) animate it onto the item's final position.
3670
+ */
3671
+ #startSettling() {
3672
+ const ended = this.dragState.endedDragState();
3673
+ if (!this.#dropAnimator?.isEnabled() || !ended?.draggedItem || !ended.cursorPosition) {
3674
+ return;
3675
+ }
3676
+ const settling = {
3677
+ item: ended.draggedItem,
3678
+ position: this.#positionFor(ended),
3679
+ };
3680
+ this.#settling.set(settling);
3681
+ // A normal drop lands in the target list; a cancel, a rejected drop or one the consumer
3682
+ // has not committed yet leaves the item in its source list.
3683
+ const droppableIds = this.dragState.wasCancelled()
3684
+ ? [ended.sourceDroppableId]
3685
+ : [ended.activeDroppableId, ended.sourceDroppableId];
3686
+ afterNextRender(() => {
3687
+ if (this.#settling() !== settling) {
3688
+ return;
3689
+ }
3690
+ const ghost = this.previewElement()?.nativeElement;
3691
+ if (!ghost || !this.#dropAnimator) {
3692
+ this.#settling.set(null);
3693
+ return;
3694
+ }
3695
+ const target = findDropTarget(settling.item.draggableId, droppableIds);
3696
+ this.#dropAnimator.play(ghost, target, () => {
3697
+ if (this.#settling() === settling) {
3698
+ this.#settling.set(null);
3699
+ }
3700
+ });
3701
+ }, { injector: this.#injector });
3702
+ }
3703
+ #stopSettling() {
3704
+ this.#dropAnimator?.cancel();
3705
+ this.#settling.set(null);
3706
+ }
3490
3707
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DragPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3491
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: DragPreviewComponent, isStandalone: true, selector: "vdnd-drag-preview", inputs: { previewTemplate: { classPropertyName: "previewTemplate", publicName: "previewTemplate", isSignal: true, isRequired: false, transformFunction: null }, cursorOffset: { classPropertyName: "cursorOffset", publicName: "cursorOffset", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "cloneContainer", first: true, predicate: ["cloneContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `
3708
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: DragPreviewComponent, isStandalone: true, selector: "vdnd-drag-preview", inputs: { previewTemplate: { classPropertyName: "previewTemplate", publicName: "previewTemplate", isSignal: true, isRequired: false, transformFunction: null }, cursorOffset: { classPropertyName: "cursorOffset", publicName: "cursorOffset", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "cloneContainer", first: true, predicate: ["cloneContainer"], descendants: true, isSignal: true }, { propertyName: "previewElement", first: true, predicate: ["preview"], descendants: true, isSignal: true }], ngImport: i0, template: `
3492
3709
  @if (isVisible()) {
3493
3710
  <div
3711
+ #preview
3494
3712
  class="vdnd-drag-preview"
3495
- data-testid="vdnd-drag-preview"
3713
+ [class.vdnd-drag-preview-dropping]="isSettling()"
3714
+ [attr.data-testid]="isSettling() ? 'vdnd-drag-preview-dropping' : 'vdnd-drag-preview'"
3496
3715
  [style.transform]="transform()"
3497
3716
  [style.width.px]="dimensions().width"
3498
3717
  [style.height.px]="dimensions().height"
@@ -3504,7 +3723,7 @@ class DragPreviewComponent {
3504
3723
  <div class="vdnd-drag-preview-clone" #cloneContainer></div>
3505
3724
  } @else {
3506
3725
  <div class="vdnd-drag-preview-default">
3507
- {{ dragState.draggedItem()?.draggableId }}
3726
+ {{ displayedItem()?.draggableId }}
3508
3727
  </div>
3509
3728
  }
3510
3729
  </div>
@@ -3516,8 +3735,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
3516
3735
  args: [{ selector: 'vdnd-drag-preview', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet], template: `
3517
3736
  @if (isVisible()) {
3518
3737
  <div
3738
+ #preview
3519
3739
  class="vdnd-drag-preview"
3520
- data-testid="vdnd-drag-preview"
3740
+ [class.vdnd-drag-preview-dropping]="isSettling()"
3741
+ [attr.data-testid]="isSettling() ? 'vdnd-drag-preview-dropping' : 'vdnd-drag-preview'"
3521
3742
  [style.transform]="transform()"
3522
3743
  [style.width.px]="dimensions().width"
3523
3744
  [style.height.px]="dimensions().height"
@@ -3529,13 +3750,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
3529
3750
  <div class="vdnd-drag-preview-clone" #cloneContainer></div>
3530
3751
  } @else {
3531
3752
  <div class="vdnd-drag-preview-default">
3532
- {{ dragState.draggedItem()?.draggableId }}
3753
+ {{ displayedItem()?.draggableId }}
3533
3754
  </div>
3534
3755
  }
3535
3756
  </div>
3536
3757
  }
3537
3758
  `, styles: [".vdnd-drag-preview{box-sizing:border-box;position:fixed;left:0;top:0;will-change:transform;pointer-events:none;z-index:1000}.vdnd-drag-preview-clone{width:100%;height:100%;overflow:hidden}\n"] }]
3538
- }], ctorParameters: () => [], propDecorators: { previewTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "previewTemplate", required: false }] }], cursorOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "cursorOffset", required: false }] }], cloneContainer: [{ type: i0.ViewChild, args: ['cloneContainer', { isSignal: true }] }] } });
3759
+ }], ctorParameters: () => [], propDecorators: { previewTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "previewTemplate", required: false }] }], cursorOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "cursorOffset", required: false }] }], cloneContainer: [{ type: i0.ViewChild, args: ['cloneContainer', { isSignal: true }] }], previewElement: [{ type: i0.ViewChild, args: ['preview', { isSignal: true }] }] } });
3539
3760
 
3540
3761
  /**
3541
3762
  * A placeholder component that indicates where a dropped item will be inserted.
@@ -4931,7 +5152,7 @@ var _a;
4931
5152
  * - Pointer down / move / up lifecycle
4932
5153
  * - Drag threshold detection
4933
5154
  * - Drag delay timer
4934
- * - RAF throttling for move updates
5155
+ * - Forwarding moves to the directive (DragSchedulerService coalesces them per frame)
4935
5156
  * - Document-level listener management
4936
5157
  * - Escape key cancellation during pointer drag
4937
5158
  */