ngx-virtual-dnd 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sergey Gultyayev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,605 @@
1
+ # ngx-virtual-dnd
2
+
3
+ A performant drag-and-drop library for Angular that works seamlessly with virtual scrolling. Built for large lists where traditional drag-and-drop solutions fail due to DOM virtualization.
4
+
5
+ ## Features
6
+
7
+ - Drag-and-drop with virtual scroll support
8
+ - Automatic scrolling when dragging near container edges
9
+ - Multiple droppable containers with group-based restrictions
10
+ - Mouse and touch support
11
+ - Accessible with ARIA attributes and keyboard support
12
+ - Signal-based state management
13
+ - Simplified high-level API with `VirtualSortableListComponent`
14
+ - Group inheritance with `DroppableGroupDirective`
15
+ - Utility functions for drop handling (`moveItem`, `reorderItems`)
16
+ - **External scroll container support** via `vdndScrollable` directive
17
+ - No external dependencies (except Angular)
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install ngx-virtual-dnd
23
+ ```
24
+
25
+ ## Requirements
26
+
27
+ - Angular 21+
28
+
29
+ ## Quick Start (Simplified API)
30
+
31
+ The easiest way to get started is with the high-level `VirtualSortableListComponent`:
32
+
33
+ ```typescript
34
+ import {
35
+ DragPreviewComponent,
36
+ DraggableDirective,
37
+ VirtualSortableListComponent,
38
+ DroppableGroupDirective,
39
+ PlaceholderComponent,
40
+ DropEvent,
41
+ moveItem,
42
+ } from 'ngx-virtual-dnd';
43
+
44
+ @Component({
45
+ imports: [
46
+ DragPreviewComponent,
47
+ DraggableDirective,
48
+ VirtualSortableListComponent,
49
+ DroppableGroupDirective,
50
+ PlaceholderComponent,
51
+ ],
52
+ template: `
53
+ <!-- Item template -->
54
+ <ng-template #itemTpl let-item let-isPlaceholder="isPlaceholder">
55
+ @if (isPlaceholder) {
56
+ <vdnd-placeholder [height]="50"></vdnd-placeholder>
57
+ } @else {
58
+ <div class="item" vdndDraggable="{{ item.id }}" [vdndDraggableData]="item">
59
+ {{ item.name }}
60
+ </div>
61
+ }
62
+ </ng-template>
63
+
64
+ <!-- Use vdndGroup to set group for all children -->
65
+ <div vdndGroup="my-group">
66
+ <!-- Sortable list - handles placeholder and sticky items automatically! -->
67
+ <vdnd-sortable-list
68
+ droppableId="list-1"
69
+ group="my-group"
70
+ [items]="list1()"
71
+ [itemHeight]="50"
72
+ [containerHeight]="400"
73
+ [itemIdFn]="getItemId"
74
+ [itemTemplate]="itemTpl"
75
+ (drop)="onDrop($event)"
76
+ >
77
+ </vdnd-sortable-list>
78
+
79
+ <vdnd-sortable-list
80
+ droppableId="list-2"
81
+ group="my-group"
82
+ [items]="list2()"
83
+ [itemHeight]="50"
84
+ [containerHeight]="400"
85
+ [itemIdFn]="getItemId"
86
+ [itemTemplate]="itemTpl"
87
+ (drop)="onDrop($event)"
88
+ >
89
+ </vdnd-sortable-list>
90
+ </div>
91
+
92
+ <vdnd-drag-preview></vdnd-drag-preview>
93
+ `,
94
+ })
95
+ export class MyComponent {
96
+ list1 = signal<Item[]>([]);
97
+ list2 = signal<Item[]>([]);
98
+
99
+ getItemId = (item: Item) => item.id;
100
+
101
+ // One-liner drop handler using moveItem utility!
102
+ onDrop(event: DropEvent): void {
103
+ moveItem(event, {
104
+ 'list-1': this.list1,
105
+ 'list-2': this.list2,
106
+ });
107
+ }
108
+ }
109
+ ```
110
+
111
+ ## Quick Start (Low-Level API)
112
+
113
+ For more control, use the individual directives and components:
114
+
115
+ ```typescript
116
+ import {
117
+ DragPreviewComponent,
118
+ DraggableDirective,
119
+ DroppableDirective,
120
+ VirtualScrollContainerComponent,
121
+ PlaceholderComponent,
122
+ DropEvent,
123
+ DragStateService,
124
+ } from 'ngx-virtual-dnd';
125
+
126
+ @Component({
127
+ imports: [
128
+ DragPreviewComponent,
129
+ DraggableDirective,
130
+ DroppableDirective,
131
+ VirtualScrollContainerComponent,
132
+ PlaceholderComponent,
133
+ ],
134
+ template: `
135
+ <!-- Item template with manual placeholder handling -->
136
+ <ng-template #itemTpl let-item>
137
+ @if (item.isPlaceholder) {
138
+ <vdnd-placeholder [height]="50"></vdnd-placeholder>
139
+ } @else {
140
+ <div
141
+ class="item"
142
+ vdndDraggable="{{ item.id }}"
143
+ vdndDraggableGroup="my-group"
144
+ [vdndDraggableData]="item"
145
+ >
146
+ {{ item.name }}
147
+ </div>
148
+ }
149
+ </ng-template>
150
+
151
+ <!-- Droppable container with virtual scroll -->
152
+ <div vdndDroppable="list-1" vdndDroppableGroup="my-group" (drop)="onDrop($event)">
153
+ <vdnd-virtual-scroll
154
+ [items]="itemsWithPlaceholder()"
155
+ [itemHeight]="50"
156
+ [containerHeight]="400"
157
+ [stickyItemIds]="stickyIds()"
158
+ [itemIdFn]="getItemId"
159
+ [trackByFn]="trackById"
160
+ [itemTemplate]="itemTpl"
161
+ >
162
+ </vdnd-virtual-scroll>
163
+ </div>
164
+
165
+ <vdnd-drag-preview></vdnd-drag-preview>
166
+ `,
167
+ })
168
+ export class MyComponent {
169
+ private dragState = inject(DragStateService);
170
+ items = signal<Item[]>([]);
171
+
172
+ // Must manually compute sticky IDs
173
+ stickyIds = computed(() => {
174
+ const draggedItem = this.dragState.draggedItem();
175
+ return draggedItem ? [draggedItem.draggableId] : [];
176
+ });
177
+
178
+ // Must manually insert placeholder
179
+ itemsWithPlaceholder = computed(() => {
180
+ // ... placeholder insertion logic
181
+ });
182
+
183
+ getItemId = (item: Item) => item.id;
184
+ trackById = (index: number, item: Item) => item.id;
185
+
186
+ onDrop(event: DropEvent): void {
187
+ // Manual drop handling logic
188
+ }
189
+ }
190
+ ```
191
+
192
+ ## API Reference
193
+
194
+ ### DraggableDirective
195
+
196
+ Makes an element draggable.
197
+
198
+ ```html
199
+ <div
200
+ vdndDraggable="unique-id"
201
+ vdndDraggableGroup="group-name"
202
+ [vdndDraggableData]="data"
203
+ [disabled]="false"
204
+ [dragHandle]=".handle"
205
+ [dragThreshold]="5"
206
+ (dragStart)="onDragStart($event)"
207
+ (dragMove)="onDragMove($event)"
208
+ (dragEnd)="onDragEnd($event)"
209
+ ></div>
210
+ ```
211
+
212
+ | Input | Type | Description |
213
+ | -------------------- | --------- | ------------------------------------------------ |
214
+ | `vdndDraggable` | `string` | Unique identifier for the draggable |
215
+ | `vdndDraggableGroup` | `string` | Group name for restricting drop targets |
216
+ | `vdndDraggableData` | `unknown` | Optional data attached to the draggable |
217
+ | `disabled` | `boolean` | Whether dragging is disabled |
218
+ | `dragHandle` | `string` | CSS selector for drag handle element |
219
+ | `dragThreshold` | `number` | Minimum distance before drag starts (default: 5) |
220
+
221
+ | Output | Type | Description |
222
+ | ----------- | ---------------- | ---------------------------- |
223
+ | `dragStart` | `DragStartEvent` | Emitted when drag starts |
224
+ | `dragMove` | `DragMoveEvent` | Emitted during drag movement |
225
+ | `dragEnd` | `DragEndEvent` | Emitted when drag ends |
226
+
227
+ ### DroppableDirective
228
+
229
+ Marks an element as a valid drop target.
230
+
231
+ ```html
232
+ <div
233
+ vdndDroppable="list-id"
234
+ vdndDroppableGroup="group-name"
235
+ [vdndDroppableData]="data"
236
+ [disabled]="false"
237
+ [autoScrollEnabled]="true"
238
+ [autoScrollConfig]="{ threshold: 50, maxSpeed: 15 }"
239
+ (dragEnter)="onDragEnter($event)"
240
+ (dragLeave)="onDragLeave($event)"
241
+ (dragOver)="onDragOver($event)"
242
+ (drop)="onDrop($event)"
243
+ ></div>
244
+ ```
245
+
246
+ | Input | Type | Description |
247
+ | -------------------- | ------------------ | --------------------------------------------- |
248
+ | `vdndDroppable` | `string` | Unique identifier for the droppable |
249
+ | `vdndDroppableGroup` | `string` | Group name (must match draggable group) |
250
+ | `vdndDroppableData` | `unknown` | Optional data attached to the droppable |
251
+ | `disabled` | `boolean` | Whether dropping is disabled |
252
+ | `autoScrollEnabled` | `boolean` | Enable auto-scroll near edges (default: true) |
253
+ | `autoScrollConfig` | `AutoScrollConfig` | Auto-scroll configuration |
254
+
255
+ | Output | Type | Description |
256
+ | ----------- | ---------------- | ----------------------------------------------- |
257
+ | `dragEnter` | `DragEnterEvent` | Emitted when a draggable enters |
258
+ | `dragLeave` | `DragLeaveEvent` | Emitted when a draggable leaves |
259
+ | `dragOver` | `DragOverEvent` | Emitted while hovering with placeholder updates |
260
+ | `drop` | `DropEvent` | Emitted when an item is dropped |
261
+
262
+ ### VirtualScrollContainerComponent
263
+
264
+ A virtual scroll container that only renders visible items.
265
+
266
+ ```html
267
+ <vdnd-virtual-scroll
268
+ [items]="items()"
269
+ [itemHeight]="50"
270
+ [containerHeight]="400"
271
+ [overscan]="3"
272
+ [stickyItemIds]="stickyIds()"
273
+ [itemIdFn]="getItemId"
274
+ [trackByFn]="trackById"
275
+ [itemTemplate]="itemTpl"
276
+ (visibleRangeChange)="onRangeChange($event)"
277
+ (scrollPositionChange)="onScroll($event)"
278
+ >
279
+ </vdnd-virtual-scroll>
280
+ ```
281
+
282
+ | Input | Type | Description |
283
+ | ----------------- | ------------------------------------ | ------------------------------------------------- |
284
+ | `items` | `T[]` | Array of items to render |
285
+ | `itemHeight` | `number` | Height of each item in pixels |
286
+ | `containerHeight` | `number` | Height of the container in pixels |
287
+ | `overscan` | `number` | Items to render above/below viewport (default: 3) |
288
+ | `stickyItemIds` | `string[]` | IDs of items that should always be rendered |
289
+ | `itemIdFn` | `(item: T) => string` | Function to get unique ID from item |
290
+ | `trackByFn` | `(index: number, item: T) => string` | Track-by function for the loop |
291
+ | `itemTemplate` | `TemplateRef` | Template for rendering each item |
292
+
293
+ ### VirtualForDirective with Custom Scroll Containers
294
+
295
+ For advanced use cases where you need virtual scrolling inside an external scroll container (e.g., a custom scrollable div, a framework-provided scroll host), use the `vdndScrollable` directive with `*vdndVirtualFor`:
296
+
297
+ ```html
298
+ <div vdndScrollable style="overflow: auto; height: 400px;">
299
+ <ng-container
300
+ *vdndVirtualFor="
301
+ let item of items();
302
+ itemHeight: 50;
303
+ trackBy: trackById;
304
+ droppableId: 'list-1';
305
+ let isPlaceholder = isPlaceholder
306
+ "
307
+ >
308
+ @if (isPlaceholder) {
309
+ <vdnd-placeholder [height]="50"></vdnd-placeholder>
310
+ } @else {
311
+ <div vdndDraggable="{{ item.id }}" vdndDraggableGroup="my-group">{{ item.name }}</div>
312
+ }
313
+ </ng-container>
314
+ </div>
315
+ ```
316
+
317
+ The `vdndScrollable` directive marks the element as the scroll container and provides it to `*vdndVirtualFor` via dependency injection.
318
+
319
+ #### ScrollableDirective
320
+
321
+ Marks an element as a scroll container for virtual scrolling.
322
+
323
+ ```html
324
+ <div
325
+ vdndScrollable
326
+ [scrollContainerId]="'my-scroll-container'"
327
+ [autoScrollEnabled]="true"
328
+ [autoScrollConfig]="{ threshold: 50, maxSpeed: 15 }"
329
+ style="overflow: auto; height: 400px;"
330
+ >
331
+ <!-- content with *vdndVirtualFor -->
332
+ </div>
333
+ ```
334
+
335
+ | Input | Type | Description |
336
+ | ------------------- | ------------------ | --------------------------------------------- |
337
+ | `scrollContainerId` | `string` | Optional ID for auto-scroll registration |
338
+ | `autoScrollEnabled` | `boolean` | Enable auto-scroll near edges (default: true) |
339
+ | `autoScrollConfig` | `AutoScrollConfig` | Auto-scroll configuration |
340
+
341
+ #### VirtualForDirective
342
+
343
+ A structural directive for virtual scrolling within custom scroll containers.
344
+
345
+ ```html
346
+ <ng-container
347
+ *vdndVirtualFor="
348
+ let item of items();
349
+ itemHeight: 50;
350
+ trackBy: trackById;
351
+ overscan: 3;
352
+ droppableId: 'list-1';
353
+ autoPlaceholder: true;
354
+ let index = index;
355
+ let isPlaceholder = isPlaceholder
356
+ "
357
+ >
358
+ <!-- item template -->
359
+ </ng-container>
360
+ ```
361
+
362
+ | Input | Type | Description |
363
+ | ----------------- | ------------------------------------- | --------------------------------------------- |
364
+ | `of` | `T[]` | Array of items to iterate over |
365
+ | `itemHeight` | `number` | Height of each item in pixels |
366
+ | `trackBy` | `(index: number, item: T) => unknown` | Track-by function for efficient updates |
367
+ | `overscan` | `number` | Items to render outside viewport (default: 3) |
368
+ | `droppableId` | `string` | Droppable ID for auto-placeholder support |
369
+ | `autoPlaceholder` | `boolean` | Auto-insert placeholder (default: true) |
370
+
371
+ | Context Variable | Type | Description |
372
+ | ---------------- | --------- | -------------------------------------------- |
373
+ | `$implicit` | `T` | The item data |
374
+ | `index` | `number` | Item index (-1 for placeholders) |
375
+ | `first` | `boolean` | Whether this is the first visible item |
376
+ | `last` | `boolean` | Whether this is the last visible item |
377
+ | `count` | `number` | Total item count |
378
+ | `isPlaceholder` | `boolean` | Whether this is an auto-inserted placeholder |
379
+
380
+ ### DragPreviewComponent
381
+
382
+ Renders a preview that follows the cursor during drag.
383
+
384
+ ```html
385
+ <vdnd-drag-preview [cursorOffset]="{ x: 8, y: 8 }">
386
+ <ng-template let-data let-id="draggableId" let-droppableId="droppableId">
387
+ <div class="preview">{{ data?.name }}</div>
388
+ </ng-template>
389
+ </vdnd-drag-preview>
390
+ ```
391
+
392
+ ### PlaceholderComponent
393
+
394
+ A visual placeholder indicating where the item will be inserted.
395
+
396
+ ```html
397
+ <vdnd-placeholder [height]="50"></vdnd-placeholder>
398
+ ```
399
+
400
+ ### VirtualSortableListComponent
401
+
402
+ A high-level component that combines droppable, virtual scroll, and placeholder functionality. **Recommended for most use cases.**
403
+
404
+ ```html
405
+ <vdnd-sortable-list
406
+ droppableId="list-1"
407
+ group="my-group"
408
+ [items]="items()"
409
+ [itemHeight]="50"
410
+ [itemIdFn]="getItemId"
411
+ [itemTemplate]="itemTpl"
412
+ [placeholderTemplate]="placeholderTpl"
413
+ (drop)="onDrop($event)"
414
+ >
415
+ </vdnd-sortable-list>
416
+ ```
417
+
418
+ | Input | Type | Description |
419
+ | --------------------- | --------------------- | ---------------------------------------- |
420
+ | `droppableId` | `string` | Unique identifier for this list |
421
+ | `group` | `string` | Drag-and-drop group name |
422
+ | `items` | `T[]` | Array of items to render |
423
+ | `itemHeight` | `number` | Height of each item in pixels |
424
+ | `itemIdFn` | `(item: T) => string` | Function to get unique ID from item |
425
+ | `itemTemplate` | `TemplateRef` | Template for rendering each item |
426
+ | `trackByFn` | `Function` | Optional track-by (defaults to itemIdFn) |
427
+ | `placeholderTemplate` | `TemplateRef` | Optional custom placeholder template |
428
+ | `containerHeight` | `number` | Optional explicit container height |
429
+ | `disabled` | `boolean` | Whether this list is disabled |
430
+
431
+ | Output | Type | Description |
432
+ | -------------------- | -------------------- | ---------------------------------- |
433
+ | `drop` | `DropEvent` | Emitted when an item is dropped |
434
+ | `dragEnter` | `DragEnterEvent` | Emitted when a draggable enters |
435
+ | `dragLeave` | `DragLeaveEvent` | Emitted when a draggable leaves |
436
+ | `visibleRangeChange` | `VisibleRangeChange` | Emitted when visible range changes |
437
+
438
+ ### DroppableGroupDirective
439
+
440
+ Provides group context to child draggables and droppables, eliminating repetitive `vdndDraggableGroup` and `vdndDroppableGroup` attributes.
441
+
442
+ ```html
443
+ <!-- Without group directive (verbose) -->
444
+ <div vdndDroppable="list-1" vdndDroppableGroup="my-group">
445
+ <div vdndDraggable="item-1" vdndDraggableGroup="my-group">Item 1</div>
446
+ <div vdndDraggable="item-2" vdndDraggableGroup="my-group">Item 2</div>
447
+ </div>
448
+
449
+ <!-- With group directive (concise) -->
450
+ <div vdndGroup="my-group">
451
+ <div vdndDroppable="list-1">
452
+ <div vdndDraggable="item-1">Item 1</div>
453
+ <div vdndDraggable="item-2">Item 2</div>
454
+ </div>
455
+ </div>
456
+ ```
457
+
458
+ ### Drop Utilities
459
+
460
+ Utility functions for common drop handling patterns:
461
+
462
+ ```typescript
463
+ import { moveItem, reorderItems, applyMove, isNoOpDrop } from 'ngx-virtual-dnd';
464
+
465
+ // Move items between signal-based lists
466
+ moveItem(event, {
467
+ 'list-1': this.list1,
468
+ 'list-2': this.list2,
469
+ });
470
+
471
+ // Reorder within a single list
472
+ reorderItems(event, this.items);
473
+
474
+ // Immutable version (returns new arrays)
475
+ const updated = applyMove(event, {
476
+ 'list-1': this.list1(),
477
+ 'list-2': this.list2(),
478
+ });
479
+
480
+ // Check if drop is a no-op (same position)
481
+ if (isNoOpDrop(event)) return;
482
+ ```
483
+
484
+ ### DragStateService
485
+
486
+ Central service for accessing drag state. Inject to build custom integrations.
487
+
488
+ ```typescript
489
+ @Injectable({ providedIn: 'root' })
490
+ export class DragStateService {
491
+ readonly isDragging: Signal<boolean>;
492
+ readonly draggedItem: Signal<DraggedItem | null>;
493
+ readonly sourceDroppableId: Signal<string | null>;
494
+ readonly activeDroppableId: Signal<string | null>;
495
+ readonly placeholderId: Signal<string | null>;
496
+ readonly cursorPosition: Signal<CursorPosition | null>;
497
+ }
498
+ ```
499
+
500
+ ### Custom Scroll Container (Advanced)
501
+
502
+ For advanced use cases, you can implement your own scroll container by providing the `VDND_SCROLL_CONTAINER` token:
503
+
504
+ ```typescript
505
+ import { VDND_SCROLL_CONTAINER, VdndScrollContainer } from 'ngx-virtual-dnd';
506
+
507
+ @Directive({
508
+ selector: '[myCustomScrollable]',
509
+ providers: [{ provide: VDND_SCROLL_CONTAINER, useExisting: MyCustomScrollableDirective }],
510
+ })
511
+ export class MyCustomScrollableDirective implements VdndScrollContainer {
512
+ readonly #elementRef = inject(ElementRef<HTMLElement>);
513
+ readonly #scrollTop = signal(0);
514
+ readonly #containerHeight = signal(0);
515
+
516
+ get nativeElement(): HTMLElement {
517
+ return this.#elementRef.nativeElement;
518
+ }
519
+
520
+ scrollTop(): number {
521
+ return this.#scrollTop();
522
+ }
523
+
524
+ containerHeight(): number {
525
+ return this.#containerHeight();
526
+ }
527
+
528
+ scrollTo(options: ScrollToOptions): void {
529
+ this.nativeElement.scrollTo(options);
530
+ }
531
+
532
+ // Set up scroll listeners and resize observers to update signals...
533
+ }
534
+ ```
535
+
536
+ The `VdndScrollContainer` interface requires:
537
+
538
+ | Method/Property | Type | Description |
539
+ | ------------------- | ------------------------------------ | ------------------------------------------ |
540
+ | `nativeElement` | `HTMLElement` | The scrollable DOM element |
541
+ | `scrollTop()` | `number` | Current scroll position (must be reactive) |
542
+ | `containerHeight()` | `number` | Container height (must be reactive) |
543
+ | `scrollTo()` | `(options: ScrollToOptions) => void` | Scroll to a position |
544
+
545
+ **Important:** The `scrollTop()` and `containerHeight()` methods must be backed by signals so that changes trigger re-computation in `*vdndVirtualFor`.
546
+
547
+ ## Event Types
548
+
549
+ ### DropEvent
550
+
551
+ ```typescript
552
+ interface DropEvent {
553
+ source: {
554
+ draggableId: string;
555
+ droppableId: string;
556
+ index: number;
557
+ data?: unknown;
558
+ };
559
+ destination: {
560
+ droppableId: string;
561
+ placeholderId: string;
562
+ index: number;
563
+ data?: unknown;
564
+ };
565
+ }
566
+ ```
567
+
568
+ ### AutoScrollConfig
569
+
570
+ ```typescript
571
+ interface AutoScrollConfig {
572
+ threshold: number; // Distance from edge to start scrolling (default: 50)
573
+ maxSpeed: number; // Maximum scroll speed in px/frame (default: 15)
574
+ accelerate: boolean; // Accelerate based on distance from edge (default: true)
575
+ }
576
+ ```
577
+
578
+ ## CSS Classes
579
+
580
+ The library adds CSS classes for styling drag states:
581
+
582
+ | Class | Element | Applied When |
583
+ | ------------------------- | --------- | --------------------------- |
584
+ | `vdnd-draggable` | Draggable | Always |
585
+ | `vdnd-draggable-dragging` | Draggable | While being dragged |
586
+ | `vdnd-draggable-disabled` | Draggable | When disabled |
587
+ | `vdnd-droppable` | Droppable | Always |
588
+ | `vdnd-droppable-active` | Droppable | When a draggable is over it |
589
+ | `vdnd-droppable-disabled` | Droppable | When disabled |
590
+
591
+ ## How It Works
592
+
593
+ 1. **Virtual Scrolling**: Only items in the visible viewport (plus overscan) are rendered. This allows lists with thousands of items to perform well.
594
+
595
+ 2. **Sticky Items**: During drag, the dragged item is marked as "sticky" so it remains rendered even when scrolled out of view.
596
+
597
+ 3. **Position Detection**: Uses `document.elementFromPoint()` with temporarily hidden drag preview to detect what's under the cursor.
598
+
599
+ 4. **Auto-scroll**: When dragging near container edges, the container automatically scrolls to reveal more items.
600
+
601
+ 5. **Group-based Restrictions**: Draggables can only be dropped on droppables with the same group name.
602
+
603
+ ## License
604
+
605
+ MIT