Repository: clauderic/dnd-kit Branch: main Commit: ff96f11e8406 Files: 735 Total size: 1.4 MB Directory structure: gitextract_pui9u02i/ ├── .browserslistrc ├── .changeset/ │ ├── README.md │ ├── animation-last-wins.md │ ├── autoscroller-options.md │ ├── config.json │ ├── drop-animation-source-context.md │ ├── entity-batched-id-changes.md │ ├── feedback-keyboard-transition.md │ ├── feedback-placeholder-sibling-reorder.md │ ├── feedback-transform-support.md │ ├── fix-event-type-aliases.md │ ├── fix-plugin-registry-dedup-ordering.md │ ├── fix-pointer-sensor-stale-activation.md │ ├── per-entity-plugin-config.md │ ├── refactor-style-injector.md │ ├── scroll-into-view-rewrite.md │ ├── thick-cloths-poke.md │ ├── use-deep-signal-flush-sync.md │ └── whole-cloths-warn.md ├── .eslintrc.js ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── chromatic.yml │ ├── continous-release.yml │ ├── playwright.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .vscode/ │ └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── apps/ │ ├── docs/ │ │ ├── README.md │ │ ├── concepts/ │ │ │ ├── drag-drop-manager.mdx │ │ │ ├── draggable.mdx │ │ │ ├── droppable.mdx │ │ │ └── sortable.mdx │ │ ├── docs.json │ │ ├── extend/ │ │ │ ├── modifiers.mdx │ │ │ ├── plugins/ │ │ │ │ ├── accessibility.mdx │ │ │ │ ├── auto-scroller.mdx │ │ │ │ ├── cursor.mdx │ │ │ │ ├── debug.mdx │ │ │ │ ├── feedback.mdx │ │ │ │ └── style-injector.mdx │ │ │ ├── plugins.mdx │ │ │ ├── sensors/ │ │ │ │ ├── keyboard-sensor.mdx │ │ │ │ └── pointer-sensor.mdx │ │ │ └── sensors.mdx │ │ ├── legacy/ │ │ │ ├── api-documentation/ │ │ │ │ ├── context-provider/ │ │ │ │ │ ├── collision-detection-algorithms.mdx │ │ │ │ │ ├── dnd-context.mdx │ │ │ │ │ ├── use-dnd-context.mdx │ │ │ │ │ └── use-dnd-monitor.mdx │ │ │ │ ├── draggable/ │ │ │ │ │ ├── drag-overlay.mdx │ │ │ │ │ └── use-draggable.mdx │ │ │ │ ├── draggable.mdx │ │ │ │ ├── droppable/ │ │ │ │ │ └── use-droppable.mdx │ │ │ │ ├── droppable.mdx │ │ │ │ ├── modifiers.mdx │ │ │ │ ├── sensors/ │ │ │ │ │ ├── keyboard.mdx │ │ │ │ │ ├── mouse.mdx │ │ │ │ │ ├── pointer.mdx │ │ │ │ │ └── touch.mdx │ │ │ │ └── sensors.mdx │ │ │ ├── guides/ │ │ │ │ └── accessibility.mdx │ │ │ ├── introduction/ │ │ │ │ ├── getting-started.mdx │ │ │ │ └── installation.mdx │ │ │ └── presets/ │ │ │ └── sortable/ │ │ │ ├── overview.mdx │ │ │ ├── sortable-context.mdx │ │ │ └── use-sortable.mdx │ │ ├── overview.mdx │ │ ├── quickstart.mdx │ │ ├── react/ │ │ │ ├── components/ │ │ │ │ ├── drag-drop-provider.mdx │ │ │ │ └── drag-overlay.mdx │ │ │ ├── guides/ │ │ │ │ ├── migration.mdx │ │ │ │ ├── multiple-sortable-lists.mdx │ │ │ │ └── sortable-state-management.mdx │ │ │ ├── hooks/ │ │ │ │ ├── use-drag-drop-monitor.mdx │ │ │ │ ├── use-draggable.mdx │ │ │ │ ├── use-droppable.mdx │ │ │ │ └── use-sortable.mdx │ │ │ └── quickstart.mdx │ │ ├── sandpack.js │ │ ├── snippets/ │ │ │ ├── code.mdx │ │ │ ├── quickstart/ │ │ │ │ └── intro.mdx │ │ │ ├── sandbox.mdx │ │ │ └── story.mdx │ │ ├── solid/ │ │ │ ├── components/ │ │ │ │ ├── drag-drop-provider.mdx │ │ │ │ └── drag-overlay.mdx │ │ │ ├── hooks/ │ │ │ │ ├── use-draggable.mdx │ │ │ │ ├── use-droppable.mdx │ │ │ │ └── use-sortable.mdx │ │ │ └── quickstart.mdx │ │ ├── style.css │ │ ├── svelte/ │ │ │ ├── components/ │ │ │ │ ├── drag-drop-provider.mdx │ │ │ │ └── drag-overlay.mdx │ │ │ ├── primitives/ │ │ │ │ ├── create-draggable.mdx │ │ │ │ ├── create-droppable.mdx │ │ │ │ └── create-sortable.mdx │ │ │ └── quickstart.mdx │ │ └── vue/ │ │ ├── components/ │ │ │ ├── drag-drop-provider.mdx │ │ │ └── drag-overlay.mdx │ │ ├── composables/ │ │ │ ├── use-draggable.mdx │ │ │ ├── use-droppable.mdx │ │ │ └── use-sortable.mdx │ │ └── quickstart.mdx │ ├── stories/ │ │ ├── .eslintrc.js │ │ ├── .storybook/ │ │ │ ├── main.ts │ │ │ ├── manager-head.html │ │ │ ├── manager.ts │ │ │ ├── preview-head.html │ │ │ ├── preview.tsx │ │ │ └── theme.ts │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── raw.d.ts │ │ ├── stories/ │ │ │ ├── components/ │ │ │ │ ├── docs/ │ │ │ │ │ ├── Code/ │ │ │ │ │ │ ├── Code.module.css │ │ │ │ │ │ ├── Code.tsx │ │ │ │ │ │ ├── components/ │ │ │ │ │ │ │ ├── CodeHighlighter/ │ │ │ │ │ │ │ │ ├── CodeHighlighter.module.css │ │ │ │ │ │ │ │ ├── CodeHighlighter.tsx │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Info/ │ │ │ │ │ │ ├── Info.module.css │ │ │ │ │ │ ├── Info.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Preview/ │ │ │ │ │ │ ├── Preview.module.css │ │ │ │ │ │ ├── Preview.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ └── react/ │ │ │ ├── Draggable/ │ │ │ │ ├── DragHandles/ │ │ │ │ │ ├── DragHandles.stories.tsx │ │ │ │ │ └── docs/ │ │ │ │ │ └── DragHandles.mdx │ │ │ │ ├── DragOverlay/ │ │ │ │ │ └── DragOverlay.stories.tsx │ │ │ │ ├── Draggable.stories.tsx │ │ │ │ ├── DraggableApp.tsx │ │ │ │ ├── DraggableExample.tsx │ │ │ │ ├── Modifiers/ │ │ │ │ │ ├── Modifiers.stories.tsx │ │ │ │ │ ├── SnapToGridExample.tsx │ │ │ │ │ ├── docs/ │ │ │ │ │ │ └── ModifierDocs.mdx │ │ │ │ │ └── styles.module.css │ │ │ │ ├── Sensors/ │ │ │ │ │ ├── Sensors.stories.tsx │ │ │ │ │ └── docs/ │ │ │ │ │ └── SensorDocs.mdx │ │ │ │ └── docs/ │ │ │ │ ├── DraggableDocs.mdx │ │ │ │ └── examples/ │ │ │ │ ├── Draggable.jsx │ │ │ │ ├── MultipleDraggable.jsx │ │ │ │ └── QuickStart.jsx │ │ │ ├── Droppable/ │ │ │ │ ├── Droppable.stories.tsx │ │ │ │ ├── DroppableApp.tsx │ │ │ │ ├── DroppableExample.tsx │ │ │ │ ├── MultipleDroppable/ │ │ │ │ │ ├── MultipleDroppable.stories.tsx │ │ │ │ │ └── MultipleDroppableApp.tsx │ │ │ │ └── docs/ │ │ │ │ ├── DroppableDocs.mdx │ │ │ │ └── examples/ │ │ │ │ ├── Draggable.jsx │ │ │ │ ├── Droppable.jsx │ │ │ │ ├── MultipleDroppable.jsx │ │ │ │ └── QuickStart.jsx │ │ │ ├── Resizeable/ │ │ │ │ ├── Resizeable.css │ │ │ │ ├── Resizeable.tsx │ │ │ │ └── index.ts │ │ │ ├── Sortable/ │ │ │ │ ├── CSSLayers/ │ │ │ │ │ ├── CSSLayers.stories.tsx │ │ │ │ │ └── CSSLayersExample.tsx │ │ │ │ ├── Grid/ │ │ │ │ │ ├── Grid.stories.tsx │ │ │ │ │ └── GridSortableApp.tsx │ │ │ │ ├── Horizontal/ │ │ │ │ │ ├── Horizontal.stories.tsx │ │ │ │ │ └── HorizontalSortableApp.tsx │ │ │ │ ├── Iframe/ │ │ │ │ │ ├── Iframe.stories.tsx │ │ │ │ │ └── IframeExample.tsx │ │ │ │ ├── MultipleLists/ │ │ │ │ │ ├── MultipleLists.stories.tsx │ │ │ │ │ ├── MultipleLists.tsx │ │ │ │ │ ├── MultipleListsApp.tsx │ │ │ │ │ └── docs/ │ │ │ │ │ ├── MultipleLists.mdx │ │ │ │ │ └── examples/ │ │ │ │ │ ├── Column.jsx │ │ │ │ │ ├── Item.jsx │ │ │ │ │ └── QuickStart.jsx │ │ │ │ ├── Quickstart.tsx │ │ │ │ ├── Sortable.stories.tsx │ │ │ │ ├── SortableApp.tsx │ │ │ │ ├── SortableExample.tsx │ │ │ │ ├── Table/ │ │ │ │ │ ├── Table.stories.tsx │ │ │ │ │ └── TableExample.tsx │ │ │ │ ├── Transformed/ │ │ │ │ │ ├── Transformed.stories.tsx │ │ │ │ │ └── TransformedExample.tsx │ │ │ │ ├── Tree/ │ │ │ │ │ ├── Tree.module.css │ │ │ │ │ ├── Tree.stories.tsx │ │ │ │ │ ├── Tree.tsx │ │ │ │ │ ├── TreeItem.tsx │ │ │ │ │ ├── TreeItemOverlay.tsx │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utilities.ts │ │ │ │ ├── Vertical/ │ │ │ │ │ ├── AutoScrollExample.tsx │ │ │ │ │ └── Vertical.stories.tsx │ │ │ │ ├── Virtualized/ │ │ │ │ │ ├── ReactTinyVirtualListExample.tsx │ │ │ │ │ ├── ReactVirtualExample.tsx │ │ │ │ │ ├── ReactWindowExample.tsx │ │ │ │ │ └── Virtualized.stories.tsx │ │ │ │ └── docs/ │ │ │ │ ├── SortableDocs.mdx │ │ │ │ └── examples/ │ │ │ │ ├── ControlledExample.jsx │ │ │ │ ├── Example.jsx │ │ │ │ └── UncontrolledExample.jsx │ │ │ ├── components/ │ │ │ │ ├── Actions/ │ │ │ │ │ ├── Action.tsx │ │ │ │ │ ├── Actions.module.css │ │ │ │ │ ├── Actions.tsx │ │ │ │ │ ├── Remove.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── Button/ │ │ │ │ │ └── Button.tsx │ │ │ │ ├── Container/ │ │ │ │ │ └── Container.tsx │ │ │ │ ├── Dropzone/ │ │ │ │ │ └── Dropzone.tsx │ │ │ │ ├── Grid/ │ │ │ │ │ ├── Grid.module.css │ │ │ │ │ └── Grid.tsx │ │ │ │ ├── Handle/ │ │ │ │ │ └── Handle.tsx │ │ │ │ ├── Item/ │ │ │ │ │ └── Item.tsx │ │ │ │ └── index.ts │ │ │ └── icons/ │ │ │ ├── SortableIcon.tsx │ │ │ └── index.ts │ │ ├── tests/ │ │ │ ├── drag-offset.spec.ts │ │ │ ├── draggable.spec.ts │ │ │ ├── droppable.spec.ts │ │ │ ├── sortable-autoscroll-options.spec.ts │ │ │ ├── sortable-css-layers.spec.ts │ │ │ ├── sortable-grid.spec.ts │ │ │ ├── sortable-horizontal.spec.ts │ │ │ ├── sortable-iframe.spec.ts │ │ │ ├── sortable-multiple.spec.ts │ │ │ ├── sortable-scroll.spec.ts │ │ │ ├── sortable-table.spec.ts │ │ │ ├── sortable-transformed.spec.ts │ │ │ └── sortable-vertical.spec.ts │ │ └── tsconfig.json │ ├── stories-shared/ │ │ ├── components/ │ │ │ ├── Container/ │ │ │ │ ├── Container.css │ │ │ │ ├── Container.ts │ │ │ │ └── index.ts │ │ │ ├── Item/ │ │ │ │ ├── Item.css │ │ │ │ ├── Item.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── package.json │ │ ├── preview-head.html │ │ ├── register.ts │ │ ├── styles/ │ │ │ ├── index.ts │ │ │ └── sandbox.ts │ │ ├── tests/ │ │ │ ├── drag-offset.tests.ts │ │ │ ├── draggable.tests.ts │ │ │ ├── droppable.tests.ts │ │ │ ├── fixtures.ts │ │ │ ├── sortable-transformed.tests.ts │ │ │ └── sortable-vertical.tests.ts │ │ └── utilities/ │ │ ├── classnames.ts │ │ ├── createRange.ts │ │ └── index.ts │ ├── stories-solid/ │ │ ├── .storybook/ │ │ │ ├── main.ts │ │ │ ├── manager-head.html │ │ │ ├── manager.ts │ │ │ ├── preview.ts │ │ │ └── theme.ts │ │ ├── custom-elements.d.ts │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── raw.d.ts │ │ ├── stories/ │ │ │ ├── Draggable/ │ │ │ │ ├── DragHandles/ │ │ │ │ │ ├── DragHandles.stories.tsx │ │ │ │ │ └── DragHandlesApp.tsx │ │ │ │ ├── Draggable.stories.tsx │ │ │ │ └── DraggableApp.tsx │ │ │ ├── Droppable/ │ │ │ │ ├── Droppable.stories.tsx │ │ │ │ ├── DroppableApp.tsx │ │ │ │ └── MultipleDroppable/ │ │ │ │ ├── MultipleDroppable.stories.tsx │ │ │ │ └── MultipleDroppableApp.tsx │ │ │ └── Sortable/ │ │ │ ├── Grid/ │ │ │ │ ├── Grid.stories.tsx │ │ │ │ └── GridSortableApp.tsx │ │ │ ├── Horizontal/ │ │ │ │ ├── Horizontal.stories.tsx │ │ │ │ └── HorizontalSortableApp.tsx │ │ │ ├── MultipleLists/ │ │ │ │ ├── MultipleLists.stories.tsx │ │ │ │ └── MultipleListsApp.tsx │ │ │ ├── SortableApp.tsx │ │ │ ├── SortableDragHandleApp.tsx │ │ │ └── Vertical/ │ │ │ └── Vertical.stories.tsx │ │ ├── tests/ │ │ │ ├── draggable.spec.ts │ │ │ ├── droppable.spec.ts │ │ │ └── sortable-vertical.spec.ts │ │ └── tsconfig.json │ ├── stories-svelte/ │ │ ├── .storybook/ │ │ │ ├── main.ts │ │ │ ├── manager.ts │ │ │ ├── preview.ts │ │ │ └── theme.ts │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── raw.d.ts │ │ ├── stories/ │ │ │ ├── Draggable/ │ │ │ │ ├── DragHandles/ │ │ │ │ │ ├── DragHandles.stories.ts │ │ │ │ │ ├── DragHandlesApp.svelte │ │ │ │ │ └── DraggableWithHandle.svelte │ │ │ │ ├── DragOverlay/ │ │ │ │ │ ├── DragOverlay.stories.ts │ │ │ │ │ ├── DragOverlayApp.svelte │ │ │ │ │ └── DraggableItem.svelte │ │ │ │ ├── Draggable.stories.ts │ │ │ │ ├── Draggable.svelte │ │ │ │ └── DraggableApp.svelte │ │ │ ├── Droppable/ │ │ │ │ ├── DraggableItem.svelte │ │ │ │ ├── Droppable.stories.ts │ │ │ │ ├── DroppableApp.svelte │ │ │ │ ├── DroppableZone.svelte │ │ │ │ └── MultipleDroppable/ │ │ │ │ ├── MultipleDroppable.stories.ts │ │ │ │ └── MultipleDroppableApp.svelte │ │ │ └── Sortable/ │ │ │ ├── Grid/ │ │ │ │ ├── Grid.stories.ts │ │ │ │ ├── GridSortableApp.svelte │ │ │ │ └── GridSortableItem.svelte │ │ │ ├── Horizontal/ │ │ │ │ ├── Horizontal.stories.ts │ │ │ │ ├── HorizontalSortableApp.svelte │ │ │ │ └── HorizontalSortableItem.svelte │ │ │ ├── MultipleLists/ │ │ │ │ ├── MultipleLists.stories.ts │ │ │ │ ├── MultipleListsApp.svelte │ │ │ │ ├── SortableColumn.svelte │ │ │ │ └── SortableItem.svelte │ │ │ ├── SortableApp.svelte │ │ │ ├── SortableDragHandleApp.svelte │ │ │ ├── SortableItem.svelte │ │ │ ├── SortableItemWithHandle.svelte │ │ │ └── Vertical/ │ │ │ └── Vertical.stories.ts │ │ ├── tests/ │ │ │ ├── draggable.spec.ts │ │ │ ├── droppable.spec.ts │ │ │ └── sortable-vertical.spec.ts │ │ └── vite.config.ts │ ├── stories-vanilla/ │ │ ├── .storybook/ │ │ │ ├── main.ts │ │ │ ├── manager-head.html │ │ │ ├── manager.ts │ │ │ ├── preview.ts │ │ │ └── theme.ts │ │ ├── package.json │ │ ├── raw.d.ts │ │ └── stories/ │ │ ├── Draggable/ │ │ │ ├── DragHandle/ │ │ │ │ ├── DragHandle.stories.ts │ │ │ │ └── DragHandleApp.ts │ │ │ ├── Draggable.stories.ts │ │ │ └── DraggableApp.ts │ │ ├── Droppable/ │ │ │ ├── Droppable.stories.ts │ │ │ ├── DroppableApp.ts │ │ │ └── MultipleDroppable/ │ │ │ ├── MultipleDroppable.stories.ts │ │ │ └── MultipleDroppableApp.ts │ │ └── Sortable/ │ │ ├── Grid/ │ │ │ ├── Grid.stories.ts │ │ │ └── GridSortableApp.ts │ │ ├── Horizontal/ │ │ │ ├── Horizontal.stories.ts │ │ │ └── HorizontalSortableApp.ts │ │ ├── Sortable.stories.ts │ │ └── SortableApp.ts │ └── stories-vue/ │ ├── .storybook/ │ │ ├── main.ts │ │ ├── manager-head.html │ │ ├── manager.ts │ │ ├── preview.ts │ │ └── theme.ts │ ├── package.json │ ├── playwright.config.ts │ ├── raw.d.ts │ ├── stories/ │ │ ├── Draggable/ │ │ │ ├── DragHandles/ │ │ │ │ ├── DragHandles.stories.ts │ │ │ │ └── DragHandlesApp.vue │ │ │ ├── DragOverlay/ │ │ │ │ ├── DragOverlay.stories.ts │ │ │ │ └── DragOverlayApp.vue │ │ │ ├── Draggable.stories.ts │ │ │ └── DraggableApp.vue │ │ ├── Droppable/ │ │ │ ├── Droppable.stories.ts │ │ │ ├── DroppableApp.vue │ │ │ └── MultipleDroppable/ │ │ │ ├── MultipleDroppable.stories.ts │ │ │ └── MultipleDroppableApp.vue │ │ └── Sortable/ │ │ ├── Grid/ │ │ │ ├── Grid.stories.ts │ │ │ └── GridSortableApp.vue │ │ ├── Horizontal/ │ │ │ ├── Horizontal.stories.ts │ │ │ └── HorizontalSortableApp.vue │ │ ├── MultipleLists/ │ │ │ ├── MultipleLists.stories.ts │ │ │ ├── MultipleListsApp.vue │ │ │ ├── SortableColumn.vue │ │ │ └── SortableItem.vue │ │ ├── SortableApp.vue │ │ ├── SortableDragHandleApp.vue │ │ └── Vertical/ │ │ └── Vertical.stories.ts │ └── tests/ │ ├── draggable.spec.ts │ ├── droppable.spec.ts │ └── sortable-vertical.spec.ts ├── bun.lockb ├── config/ │ └── typescript/ │ ├── base.json │ ├── react.json │ ├── solid.json │ ├── svelte.json │ ├── vanilla.json │ └── vue.json ├── package.json ├── packages/ │ ├── abstract/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── core/ │ │ │ │ ├── collision/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── notifier.ts │ │ │ │ │ ├── observer.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utilities.ts │ │ │ │ ├── entities/ │ │ │ │ │ ├── draggable/ │ │ │ │ │ │ ├── draggable.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── droppable/ │ │ │ │ │ │ ├── droppable.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── entity/ │ │ │ │ │ │ ├── entity.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── registry.ts │ │ │ │ │ │ └── types.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── manager/ │ │ │ │ │ ├── actions.ts │ │ │ │ │ ├── events.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── manager.ts │ │ │ │ │ ├── operation.ts │ │ │ │ │ ├── registry.ts │ │ │ │ │ ├── renderer.ts │ │ │ │ │ ├── status.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── modifiers/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── modifier.ts │ │ │ │ ├── plugins/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── plugin.ts │ │ │ │ │ ├── registry.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utilities.ts │ │ │ │ └── sensors/ │ │ │ │ ├── activation.ts │ │ │ │ ├── index.ts │ │ │ │ └── sensor.ts │ │ │ └── modifiers/ │ │ │ ├── axis.ts │ │ │ ├── boundingRectangle.ts │ │ │ ├── index.ts │ │ │ └── snap.ts │ │ ├── tests/ │ │ │ ├── manager-modifiers.test.ts │ │ │ └── plugin-registry.test.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── collision/ │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── algorithms/ │ │ │ │ ├── closestCenter.ts │ │ │ │ ├── closestCorners.ts │ │ │ │ ├── default.ts │ │ │ │ ├── directionBiased.ts │ │ │ │ ├── index.ts │ │ │ │ ├── pointerDistance.ts │ │ │ │ ├── pointerIntersection.ts │ │ │ │ └── shapeIntersection.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── dom/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── core/ │ │ │ │ ├── entities/ │ │ │ │ │ ├── draggable/ │ │ │ │ │ │ ├── draggable.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── droppable/ │ │ │ │ │ │ ├── droppable.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── manager/ │ │ │ │ │ ├── events.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── manager.ts │ │ │ │ ├── plugins/ │ │ │ │ │ ├── accessibility/ │ │ │ │ │ │ ├── Accessibility.ts │ │ │ │ │ │ ├── HiddenText.ts │ │ │ │ │ │ ├── LiveRegion.ts │ │ │ │ │ │ ├── defaults.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ └── utilities.ts │ │ │ │ │ ├── cursor/ │ │ │ │ │ │ ├── Cursor.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── feedback/ │ │ │ │ │ │ ├── Feedback.ts │ │ │ │ │ │ ├── constants.ts │ │ │ │ │ │ ├── dropAnimation.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── observers.ts │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ └── utilities.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── scrolling/ │ │ │ │ │ │ ├── AutoScroller.ts │ │ │ │ │ │ ├── ScrollIntent.ts │ │ │ │ │ │ ├── ScrollListener.ts │ │ │ │ │ │ ├── ScrollLock.ts │ │ │ │ │ │ ├── Scroller.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── selection/ │ │ │ │ │ │ └── PreventSelection.ts │ │ │ │ │ └── stylesheet/ │ │ │ │ │ └── StyleInjector.ts │ │ │ │ └── sensors/ │ │ │ │ ├── drag/ │ │ │ │ │ ├── DragSensor.ts │ │ │ │ │ ├── encoding.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── keyboard/ │ │ │ │ │ ├── KeyboardSensor.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── pointer/ │ │ │ │ │ ├── DelayConstraint.ts │ │ │ │ │ ├── DistanceConstraint.ts │ │ │ │ │ ├── PointerActivationConstraints.ts │ │ │ │ │ └── PointerSensor.ts │ │ │ │ └── types.ts │ │ │ ├── modifiers/ │ │ │ │ ├── RestrictToElement.ts │ │ │ │ ├── RestrictToWindow.ts │ │ │ │ └── index.ts │ │ │ ├── plugins/ │ │ │ │ └── debug/ │ │ │ │ ├── debug.ts │ │ │ │ └── index.ts │ │ │ ├── sortable/ │ │ │ │ ├── index.ts │ │ │ │ ├── plugins/ │ │ │ │ │ ├── OptimisticSortingPlugin.ts │ │ │ │ │ └── SortableKeyboardPlugin.ts │ │ │ │ ├── sortable.ts │ │ │ │ └── utilities.ts │ │ │ └── utilities/ │ │ │ ├── animations/ │ │ │ │ ├── forceFinishAnimations.ts │ │ │ │ └── getFinalKeyframe.ts │ │ │ ├── bounding-rectangle/ │ │ │ │ ├── getBoundingRectangle.ts │ │ │ │ ├── getViewportBoundingRectangle.ts │ │ │ │ ├── getVisibleBoundingRectangle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── isOverflowVisible.ts │ │ │ │ ├── isRectEqual.ts │ │ │ │ └── isVisible.ts │ │ │ ├── coordinates/ │ │ │ │ └── getEventCoordinates.ts │ │ │ ├── element/ │ │ │ │ ├── cloneElement.ts │ │ │ │ ├── getElementFromPoint.ts │ │ │ │ ├── index.ts │ │ │ │ ├── isInteractiveElement.ts │ │ │ │ └── proxiedElements.ts │ │ │ ├── event-listeners/ │ │ │ │ ├── Listeners.ts │ │ │ │ └── index.ts │ │ │ ├── execution-context/ │ │ │ │ ├── canUseDOM.ts │ │ │ │ ├── getDocument.ts │ │ │ │ ├── getDocuments.ts │ │ │ │ ├── getFixedPositionOffset.ts │ │ │ │ ├── getRoot.ts │ │ │ │ ├── getWindow.ts │ │ │ │ ├── isSafari.ts │ │ │ │ └── prefersReducedMotion.ts │ │ │ ├── frame/ │ │ │ │ ├── getFrameElement.ts │ │ │ │ ├── getFrameElements.ts │ │ │ │ └── getFrameTransform.ts │ │ │ ├── index.ts │ │ │ ├── misc/ │ │ │ │ └── generateUniqueId.ts │ │ │ ├── observers/ │ │ │ │ ├── FrameObserver.ts │ │ │ │ ├── PositionObserver.ts │ │ │ │ ├── ResizeNotifier.ts │ │ │ │ └── index.ts │ │ │ ├── popover/ │ │ │ │ ├── hidePopover.ts │ │ │ │ ├── index.ts │ │ │ │ ├── showPopover.ts │ │ │ │ └── supportsPopover.ts │ │ │ ├── scheduling/ │ │ │ │ ├── index.ts │ │ │ │ ├── scheduler.ts │ │ │ │ ├── throttle.ts │ │ │ │ └── timeout.ts │ │ │ ├── scroll/ │ │ │ │ ├── canScroll.ts │ │ │ │ ├── detectScrollIntent.ts │ │ │ │ ├── documentScrollingElement.ts │ │ │ │ ├── getScrollPosition.ts │ │ │ │ ├── getScrollableAncestors.ts │ │ │ │ ├── getScrollableElement.ts │ │ │ │ ├── index.ts │ │ │ │ ├── isFixed.ts │ │ │ │ ├── isScrollable.ts │ │ │ │ └── scrollIntoViewIfNeeded.ts │ │ │ ├── shapes/ │ │ │ │ ├── DOMRectangle.ts │ │ │ │ └── index.ts │ │ │ ├── styles/ │ │ │ │ ├── Styles.ts │ │ │ │ ├── getComputedStyles.ts │ │ │ │ └── index.ts │ │ │ ├── transform/ │ │ │ │ ├── animateTransform.ts │ │ │ │ ├── applyTransform.ts │ │ │ │ ├── computeTranslate.ts │ │ │ │ ├── index.ts │ │ │ │ ├── inverseTransform.ts │ │ │ │ ├── parseScale.ts │ │ │ │ ├── parseTransform.ts │ │ │ │ └── parseTranslate.ts │ │ │ └── type-guards/ │ │ │ ├── index.ts │ │ │ ├── isDocument.ts │ │ │ ├── isElement.ts │ │ │ ├── isHTMLElement.ts │ │ │ ├── isHTMLTableRowElement.ts │ │ │ ├── isKeyboardEvent.ts │ │ │ ├── isKeyframeEffect.ts │ │ │ ├── isNode.ts │ │ │ ├── isPointerEvent.ts │ │ │ ├── isSVGElement.ts │ │ │ ├── isShadowRoot.ts │ │ │ ├── isTextInput.ts │ │ │ ├── isWindow.ts │ │ │ ├── supportsStyle.ts │ │ │ └── supportsViewTransition.ts │ │ ├── tests/ │ │ │ ├── pointer-sensor.test.ts │ │ │ └── sortable-utilities.test.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── eslint-config/ │ │ ├── README.md │ │ └── package.json │ ├── geometry/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── distance/ │ │ │ │ ├── distance.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ ├── point/ │ │ │ │ ├── Point.ts │ │ │ │ └── index.ts │ │ │ ├── position/ │ │ │ │ ├── index.ts │ │ │ │ └── position.ts │ │ │ ├── shapes/ │ │ │ │ ├── Rectangle.ts │ │ │ │ ├── Shape.ts │ │ │ │ └── index.ts │ │ │ └── types/ │ │ │ ├── alignment.ts │ │ │ ├── axis.ts │ │ │ ├── bounding-rectangle.ts │ │ │ ├── coordinates.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── helpers/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── move.ts │ │ ├── tests/ │ │ │ └── move.test.ts │ │ └── tsconfig.json │ ├── react/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── core/ │ │ │ │ ├── context/ │ │ │ │ │ ├── DragDropProvider.tsx │ │ │ │ │ ├── context.ts │ │ │ │ │ ├── hooks.ts │ │ │ │ │ └── renderer.ts │ │ │ │ ├── draggable/ │ │ │ │ │ ├── DragOverlay.tsx │ │ │ │ │ └── useDraggable.ts │ │ │ │ ├── droppable/ │ │ │ │ │ └── useDroppable.ts │ │ │ │ ├── hooks/ │ │ │ │ │ ├── useDragDropManager.ts │ │ │ │ │ ├── useDragDropMonitor.ts │ │ │ │ │ ├── useDragOperation.ts │ │ │ │ │ └── useInstance.ts │ │ │ │ └── index.ts │ │ │ ├── hooks/ │ │ │ │ ├── index.ts │ │ │ │ ├── useComputed.ts │ │ │ │ ├── useConstant.ts │ │ │ │ ├── useDeepSignal.ts │ │ │ │ ├── useForceUpdate.ts │ │ │ │ ├── useImmediateEffect.ts │ │ │ │ ├── useIsomorphicLayoutEffect.ts │ │ │ │ ├── useLatest.ts │ │ │ │ ├── useOnElementChange.ts │ │ │ │ ├── useOnValueChange.ts │ │ │ │ └── useSignal.ts │ │ │ ├── sortable/ │ │ │ │ ├── index.ts │ │ │ │ └── useSortable.ts │ │ │ └── utilities/ │ │ │ ├── currentValue.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── solid/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── core/ │ │ │ │ ├── context/ │ │ │ │ │ ├── DragDropProvider.tsx │ │ │ │ │ ├── context.ts │ │ │ │ │ └── renderer.ts │ │ │ │ ├── draggable/ │ │ │ │ │ ├── DragOverlay.tsx │ │ │ │ │ └── useDraggable.ts │ │ │ │ ├── droppable/ │ │ │ │ │ └── useDroppable.ts │ │ │ │ ├── hooks/ │ │ │ │ │ ├── useDragDropManager.ts │ │ │ │ │ ├── useDragDropMonitor.ts │ │ │ │ │ ├── useDragOperation.ts │ │ │ │ │ └── useInstance.ts │ │ │ │ └── index.ts │ │ │ ├── hooks/ │ │ │ │ ├── index.ts │ │ │ │ └── useDeepSignal.ts │ │ │ ├── sortable/ │ │ │ │ ├── index.ts │ │ │ │ └── useSortable.ts │ │ │ └── utilities/ │ │ │ ├── index.ts │ │ │ └── saveElementPosition.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── state/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── comparators.ts │ │ │ ├── computed.ts │ │ │ ├── decorators.ts │ │ │ ├── effects.ts │ │ │ ├── history.ts │ │ │ ├── index.ts │ │ │ ├── snapshot.ts │ │ │ ├── store.ts │ │ │ └── types.ts │ │ ├── tests/ │ │ │ └── comparators.test.ts │ │ └── tsconfig.json │ ├── storybook-addon-codesandbox/ │ │ ├── package.json │ │ └── src/ │ │ ├── collect-files.ts │ │ ├── constants.ts │ │ ├── define.ts │ │ ├── index.ts │ │ ├── manager.tsx │ │ ├── preset.ts │ │ ├── preview/ │ │ │ ├── CodeSandboxButton.tsx │ │ │ ├── codesandbox-button-dom.ts │ │ │ ├── decorator-dom.ts │ │ │ └── decorator.tsx │ │ └── types.ts │ ├── svelte/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── core/ │ │ │ │ ├── context/ │ │ │ │ │ ├── DragDropProvider.svelte │ │ │ │ │ ├── context.ts │ │ │ │ │ └── renderer.svelte.ts │ │ │ │ ├── draggable/ │ │ │ │ │ ├── DragOverlay.svelte │ │ │ │ │ └── createDraggable.svelte.ts │ │ │ │ ├── droppable/ │ │ │ │ │ └── createDroppable.svelte.ts │ │ │ │ ├── hooks/ │ │ │ │ │ ├── createDragDropMonitor.svelte.ts │ │ │ │ │ ├── createDragOperation.ts │ │ │ │ │ ├── createInstance.svelte.ts │ │ │ │ │ └── getDragDropManager.ts │ │ │ │ └── index.ts │ │ │ ├── sortable/ │ │ │ │ ├── createSortable.svelte.ts │ │ │ │ └── index.ts │ │ │ └── utilities/ │ │ │ ├── createDeepSignal.svelte.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ └── vue/ │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── composables/ │ │ │ ├── index.ts │ │ │ └── useDeepSignal.ts │ │ ├── core/ │ │ │ ├── context/ │ │ │ │ ├── DragDropProvider.ts │ │ │ │ ├── context.ts │ │ │ │ └── renderer.ts │ │ │ ├── draggable/ │ │ │ │ ├── DragOverlay.ts │ │ │ │ └── useDraggable.ts │ │ │ ├── droppable/ │ │ │ │ └── useDroppable.ts │ │ │ ├── hooks/ │ │ │ │ ├── useDragDropManager.ts │ │ │ │ ├── useDragDropMonitor.ts │ │ │ │ ├── useDragOperation.ts │ │ │ │ └── useInstance.ts │ │ │ └── index.ts │ │ ├── sortable/ │ │ │ ├── index.ts │ │ │ └── useSortable.ts │ │ ├── types.ts │ │ └── utilities/ │ │ ├── context.ts │ │ ├── element.ts │ │ ├── index.ts │ │ └── ref.ts │ ├── tsconfig.json │ └── tsup.config.ts └── turbo.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .browserslistrc ================================================ defaults last 2 version not IE 11 not dead ================================================ FILE: .changeset/README.md ================================================ # Changesets Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) ================================================ FILE: .changeset/animation-last-wins.md ================================================ --- '@dnd-kit/dom': patch --- Animation resolution now uses last-wins semantics matching CSS composite order. `getFinalKeyframe` returns the last matching keyframe across all running animations instead of short-circuiting on the first match. `getProjectedTransform` collects the latest value per CSS property (`transform`, `translate`, `scale`) rather than accumulating transforms additively. ================================================ FILE: .changeset/autoscroller-options.md ================================================ --- '@dnd-kit/dom': minor --- Add `acceleration` and `threshold` options to the `AutoScroller` plugin. - `acceleration` controls the base scroll speed multiplier (default: `25`). - `threshold` controls the percentage of container dimensions that defines the scroll activation zone (default: `0.2`). Accepts a single number for both axes or `{ x, y }` for per-axis control. Setting an axis to `0` disables auto-scrolling on that axis. ```ts AutoScroller.configure({ acceleration: 15, threshold: { x: 0, y: 0.3 }, }) ``` ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@1.5.0/schema.json", "changelog": ["@changesets/changelog-github", {"repo": "clauderic/dnd-kit"}], "commit": false, "fixed": [ [ "@dnd-kit/abstract", "@dnd-kit/collision", "@dnd-kit/dom", "@dnd-kit/geometry", "@dnd-kit/helpers", "@dnd-kit/react", "@dnd-kit/state", "@dnd-kit/vue", "@dnd-kit/solid", "@dnd-kit/svelte" ] ], "access": "public", "baseBranch": "experimental", "updateInternalDependencies": "patch", "ignore": [], "snapshot": { "useCalculatedVersion": true } } ================================================ FILE: .changeset/drop-animation-source-context.md ================================================ --- '@dnd-kit/dom': minor --- The `DropAnimationFunction` context now includes `source`, providing access to the draggable entity for conditional animation logic. ```tsx Feedback.configure({ dropAnimation: async (context) => { if (context.source.type === 'service-draggable') return; // custom animation... }, }); ``` ================================================ FILE: .changeset/entity-batched-id-changes.md ================================================ --- '@dnd-kit/abstract': minor '@dnd-kit/dom': minor --- Batch entity identity changes to prevent collision oscillation during virtualized sorting. When entities swap ids (e.g. as `react-window` recycles DOM nodes during a drag), multiple registry updates could fire in an interleaved order, causing the collision detector to momentarily see stale or duplicate entries and oscillate between targets. Entity `id` changes are now deferred to a microtask and flushed atomically in a single `batch()`, ensuring: - The collision notifier skips detection while id changes are pending - The registry cleans up ghost registrations (stale keys left behind after an id swap) ================================================ FILE: .changeset/feedback-keyboard-transition.md ================================================ --- '@dnd-kit/dom': minor --- Add `keyboardTransition` option to the `Feedback` plugin for customizing or disabling the CSS transition applied when moving elements via keyboard. By default, keyboard-driven moves animate with `250ms cubic-bezier(0.25, 1, 0.5, 1)`. You can now customize the `duration` and `easing`, or set the option to `null` to disable the transition entirely. ```ts Feedback.configure({ keyboardTransition: { duration: 150, easing: 'ease-out' }, }) ``` ================================================ FILE: .changeset/feedback-placeholder-sibling-reorder.md ================================================ --- '@dnd-kit/dom': patch --- Fix Feedback plugin placeholder not repositioning when siblings are moved around a stationary source element. When a VDOM framework (e.g., Preact, Vue) reorders sibling elements during a drag operation, the source element and placeholder may remain in the DOM but no longer be adjacent. The existing `documentMutationObserver` only handled cases where the source or placeholder itself was re-added to the DOM. This adds a fallback adjacency check after processing all mutation entries, ensuring the placeholder stays next to the source element regardless of how siblings are rearranged. ================================================ FILE: .changeset/feedback-transform-support.md ================================================ --- '@dnd-kit/dom': minor --- The Feedback plugin now supports full CSS `transform` property for compatibility with libraries like react-window v2 that position elements via transforms. Transform-related CSS transitions are filtered out to prevent conflicts with Feedback-managed properties. The ResizeObserver computes shapes from CSS values rather than re-measuring the element, avoiding mid-transition measurement errors. Sortable's `animate()` cancels CSS transitions on transform-related properties before measuring to ensure correct FLIP deltas. ================================================ FILE: .changeset/fix-event-type-aliases.md ================================================ --- '@dnd-kit/abstract': minor '@dnd-kit/dom': minor '@dnd-kit/helpers': minor '@dnd-kit/react': minor '@dnd-kit/solid': minor '@dnd-kit/vue': minor '@dnd-kit/svelte': minor --- Redesign event type system to follow the DOM EventMap pattern. Introduces `DragDropEventMap` for event object types and `DragDropEventHandlers` for event handler signatures, replacing the ambiguously named `DragDropEvents`. Event type aliases (`CollisionEvent`, `DragStartEvent`, etc.) now derive directly from `DragDropEventMap` rather than using `Parameters<>` extraction. ### Migration guide - **`DragDropEvents`** has been split into two types: - `DragDropEventMap` — maps event names to event object types (like `WindowEventMap`) - `DragDropEventHandlers` — maps event names to `(event, manager) => void` handler signatures - If you were importing `DragDropEvents` to type **event objects**, use `DragDropEventMap` instead: ```ts // Before type MyEvent = Parameters['dragend']>[0]; // After type MyEvent = DragDropEventMap['dragend']; ``` - If you were importing `DragDropEvents` to type **event handlers**, use `DragDropEventHandlers` instead: ```ts // Before const handler: DragDropEvents['dragend'] = (event, manager) => {}; // After const handler: DragDropEventHandlers['dragend'] = (event, manager) => {}; ``` - The `DragDropEvents` re-export from `@dnd-kit/react` and `@dnd-kit/solid` has been removed. Import `DragDropEventMap` or `DragDropEventHandlers` from `@dnd-kit/abstract` directly if needed. - Convenience aliases (`CollisionEvent`, `DragStartEvent`, `DragEndEvent`, etc.) are unchanged and continue to work as before. ================================================ FILE: .changeset/fix-plugin-registry-dedup-ordering.md ================================================ --- '@dnd-kit/abstract': patch --- Fixed plugin registration order when deduplicating configured plugins. When a plugin was provided via `Plugin.configure()` alongside an internally-registered instance of the same plugin, the dedup logic would reorder it to the end of the registration list. This broke plugins like `Feedback` that resolve `StyleInjector` from the registry during construction, since `StyleInjector` would not yet be registered. This also prevented users from configuring `StyleInjector` with a CSP `nonce` without breaking drag feedback: ```ts plugins: (defaults) => [...defaults, StyleInjector.configure({ nonce: 'abc123' })] ``` ================================================ FILE: .changeset/fix-pointer-sensor-stale-activation.md ================================================ --- '@dnd-kit/dom': patch --- Fixed `setPointerCapture` error on touch devices caused by stale pointer activation. When a touch was released during the activation delay and followed by a quick re-touch, the pending delay timer from the first touch could fire with a stale `pointerId`, causing `setPointerCapture` to throw. The `PointerSensor` now properly aborts the activation controller during cleanup to cancel pending delay timers, and defensively handles `setPointerCapture` failures. ================================================ FILE: .changeset/per-entity-plugin-config.md ================================================ --- '@dnd-kit/abstract': minor '@dnd-kit/dom': minor '@dnd-kit/react': minor '@dnd-kit/solid': minor '@dnd-kit/vue': minor '@dnd-kit/svelte': minor --- Added per-entity plugin configuration and moved `feedback` from the Draggable entity to the Feedback plugin. Draggable entities now accept a `plugins` property for per-entity plugin configuration, using the existing `Plugin.configure()` pattern. Plugins can read per-entity options via `source.pluginConfig(PluginClass)`. The `feedback` property (`'default' | 'move' | 'clone' | 'none'`) has been moved from the Draggable entity to `FeedbackOptions`. Drop animation can also now be configured per-draggable. Plugins listed in an entity's `plugins` array are auto-registered on the manager if not already present. The Sortable class now uses this generic mechanism instead of its own custom registration logic. ### Migration guide The `feedback` property has been moved from the draggable/sortable hook input to per-entity Feedback plugin configuration. **Before:** ```tsx import { FeedbackType } from '@dnd-kit/dom'; useDraggable({ id: 'item', feedback: 'clone' }); useSortable({ id: 'item', index: 0, feedback: 'clone' }); ``` **After:** ```tsx import { Feedback } from '@dnd-kit/dom'; useDraggable({ id: 'item', plugins: [Feedback.configure({ feedback: 'clone' })], }); useSortable({ id: 'item', index: 0, plugins: [Feedback.configure({ feedback: 'clone' })], }); ``` Drop animation can now be configured per-draggable: ```tsx useDraggable({ id: 'item', plugins: [Feedback.configure({ feedback: 'clone', dropAnimation: null })], }); ``` ================================================ FILE: .changeset/refactor-style-injector.md ================================================ --- '@dnd-kit/dom': minor --- Renamed `StyleSheetManager` to `StyleInjector` and centralized CSP `nonce` configuration. The `StyleInjector` plugin now accepts a `nonce` option that is applied to all injected ` ================================================ FILE: apps/stories/.storybook/manager.ts ================================================ import {addons} from 'storybook/manager-api'; import type {API_PreparedIndexEntry} from 'storybook/internal/types'; import {theme} from './theme'; addons.setConfig({ theme, showPanel: false, }); addons.setConfig({ sidebar: { filters: { patterns: ( item: API_PreparedIndexEntry ): boolean => { return !(item.tags ?? []).includes('hidden'); }, }, }, }); ================================================ FILE: apps/stories/.storybook/preview-head.html ================================================ ================================================ FILE: apps/stories/.storybook/preview.tsx ================================================ import React from 'react'; import {Unstyled} from '@storybook/addon-docs/blocks'; import '@dnd-kit/stories-shared/register'; import {Code} from '../stories/components'; import { draggableStyles, droppableStyles, sortableStyles, multipleListsStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; import {withCodeSandbox} from '@dnd-kit/storybook-addon-codesandbox/decorator'; // Inject sandbox-compatible CSS classes into Storybook so the Example // stories render identically to their CodeSandbox counterparts. if (typeof document !== 'undefined') { const style = document.createElement('style'); style.setAttribute('data-sandbox-styles', ''); style.textContent = [draggableStyles, droppableStyles, sortableStyles, multipleListsStyles].join('\n\n'); document.head.appendChild(style); } const preview = { decorators: [withCodeSandbox], parameters: { docs: { components: { pre: (props) => (
          
        ),
        code: Code,
      },
    },
    codesandbox: {
      dependencies: {
        '@dnd-kit/abstract': 'beta',
        '@dnd-kit/dom': 'beta',
        '@dnd-kit/react': 'beta',
        '@dnd-kit/helpers': 'beta',
        '@dnd-kit/collision': 'beta',
        'react': '^19.0.0',
        'react-dom': '^19.0.0',
      },
      entry: [
        "import './styles.css';",
        "import React from 'react';",
        "import {createRoot} from 'react-dom/client';",
        "import App from './App';",
        "",
        "createRoot(document.getElementById('root')).render();",
      ].join('\n'),
      mainFile: 'src/App.tsx',
    },
    darkMode: {
      stylePreview: true,
    },
    options: {
      storySort: {
        order: [
          'Docs',
          'React',
          [
            'Draggable',
            'Droppable',
            'Sortable',
            [
              'Vertical list',
              'Horizontal list',
              'Grid',
              'Multiple lists',
              'Iframe',
              'Virtualized',
            ],
          ],
        ],
      },
    },
  },
};

export default preview;


================================================
FILE: apps/stories/.storybook/theme.ts
================================================
import {create} from 'storybook/theming/create';
import {default as brandImage} from './assets/dnd-kit-banner.svg';

export const theme = create({
  base: 'light',
  brandImage,
  appBg: '#F9F9F9',
});


================================================
FILE: apps/stories/package.json
================================================
{
  "name": "@dnd-kit/stories",
  "version": "0.0.0",
  "type": "module",
  "private": true,
  "scripts": {
    "dev": "storybook dev -p 6006",
    "build": "storybook build",
    "preview-storybook": "serve storybook-static",
    "clean": "rm -rf .turbo && rm -rf node_modules",
    "lint": "eslint ./stories/*.stories.tsx --max-warnings 0",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui"
  },
  "dependencies": {
    "@dnd-kit/abstract": "*",
    "@dnd-kit/dom": "*",
    "@dnd-kit/react": "*",
    "@dnd-kit/helpers": "*",
    "@dnd-kit/stories-shared": "*",
    "@dnd-kit/storybook-addon-codesandbox": "*",
    "@tanstack/react-virtual": "^3.0.0-beta.54",
    "clipboard": "^2.0.11",
    "prismjs": "^1.29.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-tiny-virtual-list": "^2.2.0",
    "react-window": "1.8.9"
  },
  "devDependencies": {
    "@dnd-kit/eslint-config": "*",
    "@measured/auto-frame-component": "^0.1.7",
    "@playwright/test": "^1.58.2",
    "@storybook/addon-docs": "^9.0.15",
    "@storybook/addon-links": "^9.0.15",
    "@storybook/react-vite": "^9.0.15",
    "@vitejs/plugin-react": "^4.2.1",
    "@vueless/storybook-dark-mode": "^9.0.6",
    "eslint": "^8.56.0",
    "eslint-plugin-storybook": "9.0.15",
    "http-server": "^14.1.1",
    "serve": "^14.2.4",
    "storybook": "^9.0.15",
    "typescript": "^5.7.3",
    "vite": "^6.0.0"
  }
}


================================================
FILE: apps/stories/playwright.config.ts
================================================
import {defineConfig} from '@playwright/test';

const CI = !!process.env.CI;

export default defineConfig({
  testDir: './tests',
  timeout: 15_000,
  expect: {
    timeout: 5_000,
  },
  fullyParallel: true,
  retries: CI ? 2 : 1,
  reporter: CI ? 'html' : 'list',
  use: {
    baseURL: 'http://localhost:6006',
    actionTimeout: 5_000,
  },
  projects: [
    {
      name: 'chromium',
      use: {browserName: 'chromium'},
    },
  ],
  webServer: {
    command: 'npx http-server storybook-static --port 6006 --silent',
    port: 6006,
    reuseExistingServer: true,
    timeout: 120_000,
  },
});


================================================
FILE: apps/stories/raw.d.ts
================================================
declare module '*.tsx?raw' {
  const content: string;
  export default content;
}

declare module '*.ts?raw' {
  const content: string;
  export default content;
}


================================================
FILE: apps/stories/stories/components/docs/Code/Code.module.css
================================================
.Code {
  --background-color: #263038;
  --border-radius: 8px;
  --border-color: transparent;
  --box-shadow:  0 1px 3px 0 rgba(0, 0, 0, 0.10), inset 0 0 1px hsla(203, 50%, 30%, 0.5);

  max-width: 100%;
  margin: 30px 0;
  border: 1px solid var(--border-color);
  background-color: var(--background-color);
  border-radius: var(--border-radius);
  color: #fff;
  box-shadow: var(--box-shadow);
}

.InlineCode {
  padding: 0.4em 0.6em !important;
  border-radius: 4px !important;
  color: rgb(0,37,158) !important;
  background-color: rgba(1,68,255,0.059) !important;
}


:global(.dark) .Code {
  --background-color: #2a2c2e;
  --border-color: rgba(255,255,255,0.1);
  --box-shadow: none;
}

:global(.dark) .InlineCode {
  color: rgb(135 163 255) !important;
  background-color: #2a2c2e !important;
  border-color: rgba(255,255,255,0.1);
}

.TabContent {
  padding: 20px 0;
  padding-right: 20px;
  overflow: auto;
}

.Tabs {
  display: flex;
  overflow-x: auto;
  background-color: #1e272d;
}

:global(.dark) .Tabs {
  background-color: #222425;
}

.Tab {
  background-color: transparent;
  appearance: none;
  outline: none;
  border: 0;
  color: #97999e;
  padding: 20px;
  font-family: "Roboto Mono",Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;

  cursor: pointer;
  font-size: 14px;
  font-weight: 400;
}

.Tab[aria-selected="true"] {
  color: #fff;
  background-color: var(--background-color);
}


================================================
FILE: apps/stories/stories/components/docs/Code/Code.tsx
================================================
import React, {useState} from 'react';
import {Unstyled} from '@storybook/addon-docs/blocks';

import {CodeHighlighter} from './components';
import styles from './Code.module.css';

interface Props {
  children: string | string[];
  tabs?: string[];
  className?: string;
}

export function Code(props: Props) {
  const {children, className} = props;
  const [selectedTab, setSelectedTab] = useState(0);
  const contents = Array.isArray(children) ? children[selectedTab] : children;

  return Array.isArray(children) || children.includes('\n') ? (
    
      
{props.tabs ? (
{props.tabs.map((tab, index) => ( ))}
) : null}
{contents}
) : ( {children} ); } ================================================ FILE: apps/stories/stories/components/docs/Code/components/CodeHighlighter/CodeHighlighter.module.css ================================================ @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400&display=swap'); :root { --mono-font-stack: 'Roboto Mono', Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono', 'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace; } .CodeHighlighter { position: relative; font-size: 0.9rem; } .CodeHighlighter > pre { position: relative; overflow-y: hidden; overflow-x: auto; display: flex; margin: 0; padding-left: 50px; padding-right: 20px; } .CodeHighlighter code { flex-shrink: 0; -webkit-text-size-adjust: none; text-rendering: optimizeLegibility; } .CodeHighlighter code, .LineNumbers { font-weight: 400; font-family: var(--mono-font-stack); } .Copy { position: absolute; top: -18px; right: -10px; padding: 16px; outline: none; appearance: none; border: none; background: var(--background-color); cursor: pointer; } .Copy img { opacity: 0.5; } .Copy:active { cursor: pointer; } .Copy:hover img { opacity: 1; } .LineNumbers { position: absolute; left: 20px; color: rgba(255, 255, 255, 0.2); font-weight: 300; user-select: none; } .CodeHighlighter :global .tag .token.spread .attr-value { color: #fff; } .CodeHighlighter :global .token.parameter { color: orange; } .CodeHighlighter :global .keyword, .CodeHighlighter :global .operator, .CodeHighlighter :global .tag .script.language-javascript .punctuation:not(.parentheses), .CodeHighlighter :global .tag .token.spread .punctuation { color: #eb2b78; } .CodeHighlighter :global .comment { color: #959595; } .CodeHighlighter :global .keyword.function, .CodeHighlighter :global .keyword.const, .CodeHighlighter :global .class-name, .CodeHighlighter :global .operator.arrow-function { color: #70dcee; } .CodeHighlighter :global .operator + .keyword, .CodeHighlighter :global .boolean, .CodeHighlighter :global .token.keyword.null, .CodeHighlighter :global .number, .CodeHighlighter :global .token.token.punctuation.braces, .CodeHighlighter :global .token.token.punctuation.braces + .token.punctuation.parentheses, .CodeHighlighter :global .token.token.punctuation.parentheses.opening + .token.punctuation.parentheses.opening, .CodeHighlighter :global .token.token.punctuation.parentheses + .token.parameter + .token.punctuation.parentheses { color: #fc8bf5; } .CodeHighlighter :global .string, .CodeHighlighter :global .tag .attr-value { color: #fdf0a2; } .CodeHighlighter :global .token.function + .punctuation, .CodeHighlighter :global .script.language-javascript .token.punctuation.braces, .CodeHighlighter :global .token.punctuation.parentheses, .CodeHighlighter :global .token.function ~ .token.punctuation.parentheses, .CodeHighlighter :global .token.token.punctuation.parentheses + .token.parameter + .token.punctuation.parentheses:has(+ .token.braces.opening) { color: #ffd602; } .CodeHighlighter :global .function, .CodeHighlighter :global .attr-name { color: #4bff83; } .CodeHighlighter :global .punctuation, .CodeHighlighter :global .script.language-javascript .token.punctuation.semicolon, .CodeHighlighter :global .tag .script.language-javascript { color: #f9f9f4; } .CodeHighlighter :global .tag { color: #fa2b7d; } .CodeHighlighter :global .token.punctuation.parentheses:where(+ .semicolon) { color: #ffd602 !important; } ================================================ FILE: apps/stories/stories/components/docs/Code/components/CodeHighlighter/CodeHighlighter.tsx ================================================ import React, {useEffect, useMemo, useRef} from 'react'; import Prism from 'prismjs'; import Clipboard from 'clipboard'; import 'prismjs/components/prism-jsx.min'; import copy from './copy.svg'; import styles from './CodeHighlighter.module.css'; import {classNames, createRange} from '@dnd-kit/stories-shared/utilities'; interface Props { children: string; language?: string; } export function CodeHighlighter({children = '', language = 'jsx'}: Props) { const lineCount = children.split('\n').length - 1; const nodeRef = useRef(null); const highlightedCode = useMemo( () => syntaxReplacements( Prism.highlight( children.trim(), Prism.languages[language] ?? Prism.languages.txt, language ) ), [children] ); useEffect(() => { const clipboard = new Clipboard(nodeRef.current as Element); return () => clipboard.destroy(); }, []); return (
        
        
      
); } function syntaxReplacements(value: string) { const markup = (string, newAttribute = '') => `${string}`; const replacements = [ ['const', 'token', 'const'], ['null', 'token', 'null'], ['function', 'token', 'function'], ['[(]', 'punctuation', 'parentheses opening', '('], ['[)]', 'punctuation', 'parentheses closing', ')'], ['{', 'punctuation', 'braces opening'], ['}', 'punctuation', 'braces closing'], [';', 'punctuation', 'semicolon'], ['=>', 'operator', 'arrow-function'], ]; return replacements.reduce( (accumulator, [value, label, annotation, replacement]) => accumulator.replace( new RegExp(markup(value, label), 'g'), markup(replacement ?? value, `${label} ${annotation}`) ), value ); } ================================================ FILE: apps/stories/stories/components/docs/Code/components/CodeHighlighter/index.ts ================================================ export {CodeHighlighter} from './CodeHighlighter'; ================================================ FILE: apps/stories/stories/components/docs/Code/components/index.ts ================================================ export {CodeHighlighter} from './CodeHighlighter'; ================================================ FILE: apps/stories/stories/components/docs/Code/index.ts ================================================ export {Code} from './Code'; ================================================ FILE: apps/stories/stories/components/docs/Info/Info.module.css ================================================ .Info { display: flex; gap: 10px; align-items: center; justify-content: stretch; padding: 16px 24px; color: #333; background-color: #f9f9f9; border: 1px solid #57595a42; border-radius: 10px; } :global(.dark) .Info { background-color: #ffffff05; } .Info > p { color: inherit; margin: 0; } .Info > svg { color: #2196F3; width: 20px; height: 20px; } ================================================ FILE: apps/stories/stories/components/docs/Info/Info.tsx ================================================ import React, {type PropsWithChildren} from 'react'; import styles from './Info.module.css'; const icon = ( ); export function Info({children}: PropsWithChildren) { return (
{icon}

{children}

); } ================================================ FILE: apps/stories/stories/components/docs/Info/index.ts ================================================ export {Info} from './Info'; ================================================ FILE: apps/stories/stories/components/docs/Preview/Preview.module.css ================================================ .Preview { --background-color: #FCFCFC; overflow-y: auto; padding: 30px 40px; margin: 25px 0 40px; border-radius: 8px; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.10), inset 0 0 1px hsla(203, 50%, 30%, 0.5); background: var(--background-color); } .Preview:has( + .Code) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; margin-bottom: 0; } .Preview + .Code > div > div { margin-top: 0px; border-top-left-radius: 0; border-top-right-radius: 0; } .hero { position: relative; --gradient: linear-gradient(65deg, #56fff420, #001AFF20, #5F6AF220, #F25FD020, #56FFF520, #F25FD020, #001AFF20, #56fff420); display: grid; align-items: center; min-height: 200px; max-height: 400px; background-image: var(--gradient); animation: gradient 16s linear infinite; animation-direction: alternate; background-size: 600% 100%; } .hero > * { position: relative; z-index: 1; } :global(.dark) .hero { --gradient: linear-gradient(65deg, #56fff480, #001AFF80, #5F6AF280, #F25FD080, #56FFF580, #F25FD080, #001AFF80, #56fff480); animation: background 16s linear infinite, gradient 60s linear infinite; } :global(.dark) .Preview { --background-color: #38393c; } @keyframes background { 0% { background-color: #ff0000; } 25% { background-color: #7f00ff; } 50% { background-color: #00affa; } 75% { background-color: #00f594; } 100% { background-color: #ff0000 } } @keyframes gradient { 0% {background-position: 0%} 100% {background-position: 100%} } ================================================ FILE: apps/stories/stories/components/docs/Preview/Preview.tsx ================================================ import React from 'react'; import {Story, Unstyled} from '@storybook/addon-docs/blocks'; import {type StoryFn} from '@storybook/react'; import {classNames} from '@dnd-kit/stories-shared/utilities'; import {Code} from '../Code'; import styles from './Preview.module.css'; interface Props { of?: StoryFn; code?: string; id?: string; hero?: boolean; tabs?: string[]; children?: React.ReactNode; } export function Preview({children, code, of, hero, id, tabs}: Props) { return (
{children ?? }
{code ? (
{code}
) : null}
); } ================================================ FILE: apps/stories/stories/components/docs/Preview/index.ts ================================================ export {Preview} from './Preview'; ================================================ FILE: apps/stories/stories/components/docs/index.ts ================================================ export {Code} from './Code'; export {Info} from './Info'; export {Preview} from './Preview'; ================================================ FILE: apps/stories/stories/components/index.ts ================================================ export {Code, Info, Preview} from './docs'; ================================================ FILE: apps/stories/stories/react/Draggable/DragHandles/DragHandles.stories.tsx ================================================ import type {Meta, StoryObj} from '@storybook/react-vite'; import docs from './docs/DragHandles.mdx'; import {DraggableExample} from '../DraggableExample.tsx'; import {PointerSensor, KeyboardSensor} from '@dnd-kit/dom'; const meta: Meta = { title: 'React/Draggable/Drag handles', component: DraggableExample, tags: ['autodocs'], parameters: { docs: { page: docs, }, }, }; export default meta; type Story = StoryObj; export const DragHandle: Story = { name: 'Example', args: { handle: true, }, }; export const DualActivators: Story = { name: 'Dual activators', args: { handle: true, sensors: [ PointerSensor.configure({ activatorElements(source) { return [source.handle, source.element]; }, }), KeyboardSensor, ], }, }; ================================================ FILE: apps/stories/stories/react/Draggable/DragHandles/docs/DragHandles.mdx ================================================ import {RestrictToElement} from '@dnd-kit/dom/modifiers'; import {Info, Preview} from '../../../../components'; import {DragHandle} from '../DragHandles.stories.tsx'; # Drag handles Restrict dragging to a specific element within a draggable element. ## Usage To create a drag handle, provide a `handle` element to the `useDraggable` hook by consuming the `handleRef` from the output object of the `useDraggable` hook. ```jsx import {useDraggable} from '@dnd-kit/react'; function Draggable(props) { const {handleRef, ref} = useDraggable({ id: props.id, }); return (
{props.children}
); } ``` Alternatively, you can also pass an `Element` or `ref` to an element as an argument to the `handle` option of `useDraggable`: ```jsx import {useDraggable} from '@dnd-kit/react'; function Draggable(props) { const handleRef = useRef(null); const {ref} = useDraggable({ id: props.id, handle: handleRef, }); return (
{props.children}
); } ``` ================================================ FILE: apps/stories/stories/react/Draggable/DragOverlay/DragOverlay.stories.tsx ================================================ import type {Meta, StoryObj} from '@storybook/react-vite'; import {DraggableExample} from '../DraggableExample.tsx'; const meta: Meta = { title: 'React/Draggable/Drag overlay', component: DraggableExample, }; export default meta; type Story = StoryObj; export const DragOverlay: Story = { name: 'Example', args: { overlay: true, }, }; ================================================ FILE: apps/stories/stories/react/Draggable/Draggable.stories.tsx ================================================ import type {Meta, StoryObj} from '@storybook/react-vite'; import DraggableApp from './DraggableApp.tsx'; import draggableSource from './DraggableApp.tsx?raw'; import {baseStyles, draggableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; import docs from './docs/DraggableDocs.mdx'; const meta: Meta = { title: 'React/Draggable', component: DraggableApp, tags: ['autodocs'], parameters: { docs: { page: docs, }, codesandbox: { files: { 'src/App.tsx': draggableSource, 'src/styles.css': [baseStyles, draggableStyles].join('\n\n'), }, }, }, }; export default meta; type Story = StoryObj; export const Example: Story = { name: 'Example', }; ================================================ FILE: apps/stories/stories/react/Draggable/DraggableApp.tsx ================================================ import React from 'react'; import {DragDropProvider, useDraggable} from '@dnd-kit/react'; function Draggable() { const {ref} = useDraggable({id: 'draggable'}); return ; } export default function App() { return ( ); } ================================================ FILE: apps/stories/stories/react/Draggable/DraggableExample.tsx ================================================ import React, { type PropsWithChildren, type CSSProperties, useRef, useState, } from 'react'; import type {Modifiers, Plugins, Sensors} from '@dnd-kit/abstract'; import {DragDropProvider, DragOverlay, useDraggable} from '@dnd-kit/react'; import {Button, Handle} from '../components'; interface Props { container?: React.FC> | string; handle?: boolean; overlay?: boolean; modifiers?: Modifiers; sensors?: Sensors; } export function DraggableExample(props: Props) { const {container, overlay, sensors} = props; const Wrapper = container ?? 'div'; return ( {overlay && ( )} ); } interface DraggableProps { id: string; handle?: boolean; plugins?: Plugins; modifiers?: Modifiers; style?: CSSProperties; overlay?: boolean; } export function Draggable({ id, modifiers, handle, plugins, style, overlay, }: DraggableProps) { const [element, setElement] = useState(null); const handleRef = useRef(null); const {isDragging} = useDraggable({ id, modifiers, element, plugins, handle: handleRef, }); return ( ); } ================================================ FILE: apps/stories/stories/react/Draggable/Modifiers/Modifiers.stories.tsx ================================================ import type {Meta, StoryObj} from '@storybook/react-vite'; import { RestrictToHorizontalAxis, RestrictToVerticalAxis, } from '@dnd-kit/abstract/modifiers'; import {RestrictToElement, RestrictToWindow} from '@dnd-kit/dom/modifiers'; import docs from './docs/ModifierDocs.mdx'; import {DraggableExample} from '../DraggableExample'; import {SnapToGridExample} from './SnapToGridExample'; import styles from './styles.module.css'; const meta: Meta = { title: 'React/Draggable/Modifiers', component: DraggableExample, tags: ['autodocs'], parameters: { docs: { page: docs, }, }, }; export default meta; type Story = StoryObj; export const VerticalAxis: Story = { name: 'Vertical axis', args: { modifiers: [RestrictToVerticalAxis], }, }; export const HorizontalAxis: Story = { name: 'Horizontal axis', args: { modifiers: [RestrictToHorizontalAxis], }, }; export const WindowModifier: Story = { name: 'Restrict to window', args: { modifiers: [RestrictToWindow], }, }; export const ContainerModifier: Story = { name: 'Restrict to container', args: { container({children}) { return (
{children}
); }, modifiers: [ RestrictToElement.configure({ element() { return document.querySelector('[data-container]'); }, }), ], }, }; export const SnapModifierExample: Story = { name: 'Snap to grid', render: SnapToGridExample, }; ================================================ FILE: apps/stories/stories/react/Draggable/Modifiers/SnapToGridExample.tsx ================================================ import {useState} from 'react'; import {RestrictToWindow} from '@dnd-kit/dom/modifiers'; import {SnapModifier} from '@dnd-kit/abstract/modifiers'; import {DragDropProvider} from '@dnd-kit/react'; import {Grid} from '../../components/index.ts'; import {Draggable} from '../DraggableExample.tsx'; export function SnapToGridExample() { const [gridSize, setGridSize] = useState(30); const [position, setPosition] = useState({x: gridSize, y: gridSize}); return ( { setPosition(({x, y}) => ({ x: x + event.operation.transform.x, y: y + event.operation.transform.y, })); }} > ); } ================================================ FILE: apps/stories/stories/react/Draggable/Modifiers/docs/ModifierDocs.mdx ================================================ import {RestrictToElement} from '@dnd-kit/dom/modifiers'; import {Info, Preview} from '../../../../components'; import {DraggableExample} from '../../DraggableExample'; # Modifiers Modify or restrict the behavior of draggable elements. Modifiers let you dynamically modify the movement coordinates that are detected by sensors. They can be used for a wide range of use cases, for example: - Restricting motion to a single axis - Restricting motion to the draggable node container's bounding rectangle - Restricting motion to the draggable node's scroll container bounding rectangle - Applying resistance or clamping the motion ## Usage Modifiers can be applied globally or to individual draggable elements. ### Global modifiers Modifiers can be applied globally by passing them to the `` component. ```jsx import {DragDropProvider} from '@dnd-kit/react'; import {RestrictToVerticalAxis} from '@dnd-kit/abstract/modifiers'; import {RestrictToWindow} from '@dnd-kit/dom/modifiers'; function App() { return ( {...} ) } ``` ### Local modifiers Local modifiers take precedence over global modifiers. Modifiers can also be applied to individual draggable elements by passing them to the `modifiers` prop. ```jsx import {useDraggable} from '@dnd-kit/react'; import {RestrictToVerticalAxis} from '@dnd-kit/abstract/modifiers'; import {RestrictToWindow} from '@dnd-kit/dom/modifiers'; function Draggable({id}) { const {ref} = useDraggable({ id, modifiers: [RestrictToVerticalAxis, RestrictToWindow], }); } ``` ================================================ FILE: apps/stories/stories/react/Draggable/Modifiers/styles.module.css ================================================ .Container { display: flex; width: 60%; min-width: 300px; margin: 40px 80px; height: 350px; outline: 3px solid rgba(0,0,0,0.2); background: #FFF; align-items: center; justify-content: center; padding: 30px; border-radius: 8px; } :global(.dark) .Container { outline: 1px solid rgba(255,255,255,0.3); background: rgba(255,255,255,0.05); } ================================================ FILE: apps/stories/stories/react/Draggable/Sensors/Sensors.stories.tsx ================================================ import type {Meta, StoryObj} from '@storybook/react-vite'; import docs from './docs/SensorDocs.mdx'; import {DraggableExample} from '../DraggableExample'; const meta: Meta = { title: 'React/Draggable/Sensors', component: DraggableExample, tags: ['autodocs'], parameters: { docs: { page: docs, }, }, }; export default meta; type Story = StoryObj; export const Pointer: Story = { name: 'Default sensors', }; ================================================ FILE: apps/stories/stories/react/Draggable/Sensors/docs/SensorDocs.mdx ================================================ import {Info, Preview} from '../../../../components'; import {DraggableExample} from '../../DraggableExample'; # Sensors Detect user input and translate them into drag operations. ## Overview Sensors are an abstraction to detect different input methods in order to initiate drag operations, respond to movement and end or cancel the operation. ## Built-in sensors The `@dnd-kit/dom` package provides built-in sensors that can be used to detect user input in the browser. ### Pointer sensor The Pointer sensor responds to [Pointer events](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events). This sensor is enabled by default. > Pointer events are DOM events that are fired for a pointing device. They are designed to create a single DOM event model to handle pointing input devices such as a mouse, pen/stylus or touch (such as one or more fingers). > The pointer is a hardware-agnostic device that can target a specific set of screen coordinates. > > – Source: [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events) #### Activation constraints The Pointer sensor has two types of activation constraints: - Distance constraint - Delay constraint These activation constraints are not mutually exclusive, they can be used simultaneously. In case both constraints are used, the Pointer sensor will activate as soon as one of the constraints is met. ```js import {PointerSensor, PointerActivationConstraints} from '@dnd-kit/dom'; const sensors = [ PointerSensor.configure({ activationConstraints: [ new PointerActivationConstraints.Delay({value: 200, tolerance: 10}), new PointerActivationConstraints.Distance({value: 5}), ], }), ]; ``` The **distance** `value` property represents the distance, in _pixels_, that a pointer needs to be moved by before a drag operation is initiated. The **delay** `value` property represents the duration, in _milliseconds_, that a draggable item needs to be held by the primary pointer for before a drag start event is emitted. The `tolerance` property represents the distance, in pixels, of motion that is tolerated before the drag operation is aborted. If the pointer is moved during the delay duration and the tolerance is set to zero, the drag operation will be immediately aborted. If a higher tolerance is set, for example, a tolerance of 5 pixels, the operation will only be aborted if the pointer is moved by more than 5 pixels during the delay. This property is particularly useful for touch input, where some tolerance should be accounted for when using a delay constraint, as touch input is less precise than mouse input. #### Default behavior By default, the `PointerSensor` applies sensible activation constraints depending on the context: - Mouse on handle: activates immediately. - Touch: `Delay({value: 250, tolerance: 5})`. - Text inputs: `Delay({value: 200, tolerance: 0})`. - Other cases: `Delay({value: 200, tolerance: 10})` and `Distance({value: 5})`. You can override this with `activationConstraints`, which may be an array of constraints or a function `(event, source) => ActivationConstraints | undefined` to compute them per interaction. ```js import {PointerSensor, PointerActivationConstraints} from '@dnd-kit/dom'; PointerSensor.configure({ activationConstraints(event, source) { if (event.pointerType === 'touch') { return [ new PointerActivationConstraints.Delay({value: 300, tolerance: 8}), ]; } return [new PointerActivationConstraints.Distance({value: 6})]; }, }); ``` #### Activator elements You may specify additional elements that can start a pointer interaction via `activatorElements`: ```js import {PointerSensor} from '@dnd-kit/dom'; PointerSensor.configure({ activatorElements(source) { return [ source.handle, document.querySelector('[data-dnd-activator]'), ]; }, }); ``` ### Keyboard sensor The Keyboard sensor responds to [Keyboard events](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent). This sensor is enabled by default. The keyboard sensor activates when the `handle` or `element` of a draggable source is focused and the `Space` or `Enter` key is pressed. #### Configuration The Keyboard sensor can be configured to respond to different keyboard codes: ```js import {KeyboardSensor} from '@dnd-kit/dom'; const sensors = [ KeyboardSensor.configure({ keyboardCodes: { start: ['Space', 'Enter'], cancel: ['Escape'], end: ['Space', 'Enter'], up: ['ArrowUp'], down: ['ArrowDown'], left: ['ArrowLeft'], right: ['ArrowRight'], }, }), ]; ``` You may customize these values, but do keep in mind that the [third rule of ARIA](https://www.w3.org/TR/using-aria/#3rdrule) requires that a user must be able to activate the action associated with a draggable widget using both the enter (on Windows) or return (on macOS) and the space key. To learn more, read the in-depth accessibility guide. ## Usage Sensors can be applied globally or to individual draggable elements. ### Global sensors Sensors can be applied globally by passing them to the `` component. ```jsx import {DragDropProvider} from '@dnd-kit/react'; import {PointerSensor, KeyboardSensor} from '@dnd-kit/dom'; function App() { return ( {...} ) } ``` ### Local sensors Local sensors take precedence over global sensors. Sensors can also be applied to individual draggable elements by passing them to the `sensors` prop. ```jsx import {useDraggable} from '@dnd-kit/react'; import {KeyboardSensor} from '@dnd-kit/dom'; function Draggable({id}) { const {ref} = useDraggable({ id, sensors: [KeyboardSensor], }); } ``` ================================================ FILE: apps/stories/stories/react/Draggable/docs/DraggableDocs.mdx ================================================ import {Preview} from '../../../components'; import {Example as Hero} from '../Draggable.stories'; import {Example} from './examples/QuickStart'; import ExampleSource from './examples/QuickStart?raw'; import DraggableSource from './examples/Draggable?raw'; import {Example as MultipleDraggableExample} from './examples/MultipleDraggable'; import MultipleDraggableExampleSource from './examples/MultipleDraggable?raw'; # Draggable Make elements draggable and drop them over targets. ## Usage Use the `useDraggable` hook to create draggable elements that `draggable` elements can be dropped over `droppable` targets. ```jsx import {useDraggable} from '@dnd-kit/react'; function Draggable(props) { const {ref} = useDraggable({ id: props.id, }); return ; } ``` ## Getting started To get started, we will create two files, `App.js` and `Draggable.js`. To render multiple draggable elements, we can simply render the `` component multiple times: ================================================ FILE: apps/stories/stories/react/Draggable/docs/examples/Draggable.jsx ================================================ import React from 'react'; import {useDraggable} from '@dnd-kit/react'; export function Draggable({id}) { const {ref} = useDraggable({ id, }); return ( ); } ================================================ FILE: apps/stories/stories/react/Draggable/docs/examples/MultipleDraggable.jsx ================================================ import React from 'react'; import {DragDropProvider} from '@dnd-kit/react'; import {Draggable} from './Draggable'; export function Example() { return (
); } ================================================ FILE: apps/stories/stories/react/Draggable/docs/examples/QuickStart.jsx ================================================ import React from 'react'; import {DragDropProvider} from '@dnd-kit/react'; import {Draggable} from './Draggable'; export function Example() { return ( ); } ================================================ FILE: apps/stories/stories/react/Droppable/Droppable.stories.tsx ================================================ import type {Meta, StoryObj} from '@storybook/react-vite'; import DroppableApp from './DroppableApp.tsx'; import droppableSource from './DroppableApp.tsx?raw'; import {baseStyles, draggableStyles, droppableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; import docs from './docs/DroppableDocs.mdx'; const meta: Meta = { title: 'React/Droppable', component: DroppableApp, tags: ['autodocs'], parameters: { docs: { page: docs, }, codesandbox: { files: { 'src/App.tsx': droppableSource, 'src/styles.css': [baseStyles, draggableStyles, droppableStyles].join('\n\n'), }, }, }, }; export default meta; type Story = StoryObj; export const Example: Story = { name: 'Example', }; ================================================ FILE: apps/stories/stories/react/Droppable/DroppableApp.tsx ================================================ import React, {useState} from 'react'; import {DragDropProvider, useDraggable, useDroppable} from '@dnd-kit/react'; function Draggable({id}: {id: string}) { const {ref} = useDraggable({id}); return ; } function Droppable({id, children}: {id: string; children?: React.ReactNode}) { const {ref, isDropTarget} = useDroppable({id}); return (
{children}
); } export default function App() { const [parent, setParent] = useState(undefined); const draggable = ; return ( { if (event.canceled) return; setParent(event.operation.target?.id as string); }} >
{parent == null ? draggable : null} {parent === 'droppable' ? draggable : null}
); } ================================================ FILE: apps/stories/stories/react/Droppable/DroppableExample.tsx ================================================ import React, {useState} from 'react'; import type {PropsWithChildren} from 'react'; import type {UniqueIdentifier} from '@dnd-kit/abstract'; import {DragDropProvider, useDraggable, useDroppable} from '@dnd-kit/react'; import {Debug} from '@dnd-kit/dom/plugins/debug'; import {createRange} from '@dnd-kit/stories-shared/utilities'; import {Button} from '../components/Button/Button.tsx'; import {Dropzone} from '../components/Dropzone/Dropzone.tsx'; interface Props { droppableCount?: number; debug?: boolean; } export function DroppableExample({droppableCount = 1, debug}: Props) { const [parent, setParent] = useState(); const draggable = ; return ( [...defaults, Debug] : undefined} onDragEnd={(event) => { const {target} = event.operation; if (event.canceled) { return; } setParent(target?.id); }} >
{parent == null ? draggable : null}
{createRange(droppableCount).map((id) => ( {parent === id ? draggable : null} ))}
); } interface DraggableProps { id: UniqueIdentifier; } function Draggable({id}: DraggableProps) { const [element, setElement] = useState(null); const {isDragging} = useDraggable({ id, element, }); return ( ); } interface DroppableProps { id: UniqueIdentifier; } function Droppable({id, children}: PropsWithChildren) { const {ref, isDropTarget} = useDroppable({id}); return ( {children} ); } ================================================ FILE: apps/stories/stories/react/Droppable/MultipleDroppable/MultipleDroppable.stories.tsx ================================================ import type {Meta, StoryObj} from '@storybook/react-vite'; import MultipleDroppableApp from './MultipleDroppableApp'; import multipleDroppableSource from './MultipleDroppableApp.tsx?raw'; import {baseStyles, draggableStyles, droppableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'React/Droppable/Multiple drop targets', component: MultipleDroppableApp, }; export default meta; type Story = StoryObj; export const Example: Story = { parameters: { codesandbox: { files: { 'src/App.tsx': multipleDroppableSource, 'src/styles.css': [baseStyles, draggableStyles, droppableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories/stories/react/Droppable/MultipleDroppable/MultipleDroppableApp.tsx ================================================ import React, {useState} from 'react'; import {DragDropProvider, useDraggable, useDroppable} from '@dnd-kit/react'; function Draggable({id}: {id: string}) { const {ref} = useDraggable({id}); return ; } function Droppable({id, children}: {id: string; children?: React.ReactNode}) { const {ref, isDropTarget} = useDroppable({id}); return (
{children}
); } export default function App() { const [parent, setParent] = useState(undefined); const draggable = ; const droppables = ['A', 'B', 'C']; return ( { if (event.canceled) return; setParent(event.operation.target?.id as string); }} >
{parent == null ? draggable : null}
{droppables.map((id) => ( {parent === id ? draggable : null} ))}
); } ================================================ FILE: apps/stories/stories/react/Droppable/docs/DroppableDocs.mdx ================================================ import {Preview} from '../../../components'; import {Example as Hero} from '../Droppable.stories'; import {Example} from './examples/QuickStart'; import ExampleSource from './examples/QuickStart?raw'; import DraggableSource from './examples/Draggable?raw'; import DroppableSource from './examples/Droppable?raw'; import {Example as MultipleDroppableExample} from './examples/MultipleDroppable'; import MultipleDroppableExampleSource from './examples/MultipleDroppable?raw'; # Droppable Create droppable targets for draggable elements. ## Usage Use the `useDroppable` hook to create droppable targets that `draggable` elements can be dropped over. ```jsx import {useDroppable} from '@dnd-kit/react'; function Droppable(props) { const {ref} = useDroppable({ id: props.id, }); return
{props.children}
; } ``` On its own, the `useDroppable` hook does not provide a lot of functionality. It is meant to be used in conjunction with the `useDraggable` hook, along with a parent `` component to listen for and respond to drag and drop events. You can set up as many droppable targets as needed, either by calling the `useDroppable` hook multiple times within the same component, or by setting up a generic component that calls `useDroppable` and rendering that component multiple times. Just remember to make sure they all have a unique `id` so that they can be differentiated. ## Getting started To get started, we will create three files, `App.js`, `Droppable.js`, and `Draggable.js`. To render multiple droppable targets, we can simply render the `` component multiple times: ================================================ FILE: apps/stories/stories/react/Droppable/docs/examples/Draggable.jsx ================================================ import React from 'react'; import {useDraggable} from '@dnd-kit/react'; export function Draggable({id}) { const {ref} = useDraggable({ id, }); return ( ); } ================================================ FILE: apps/stories/stories/react/Droppable/docs/examples/Droppable.jsx ================================================ import React from 'react'; import {useDroppable} from '@dnd-kit/react'; export function Droppable({id, children}) { const {ref, isDropTarget} = useDroppable({id}); return (
{children}
); } ================================================ FILE: apps/stories/stories/react/Droppable/docs/examples/MultipleDroppable.jsx ================================================ import React, {useState} from 'react'; import {DragDropProvider} from '@dnd-kit/react'; import {Droppable} from './Droppable'; import {Draggable} from './Draggable'; export function Example() { const [parent, setParent] = useState(); const parents = ['A', 'B', 'C']; const draggable = ; return ( { const {target} = event.operation; if (event.canceled) return; setParent(target ? target.id : undefined); }} >
{parent == null ? draggable : null}
{parents.map((id) => ( {parent === id ? draggable : null} ))}
); } ================================================ FILE: apps/stories/stories/react/Droppable/docs/examples/QuickStart.jsx ================================================ import React, {useState} from 'react'; import {DragDropProvider} from '@dnd-kit/react'; import {Droppable} from './Droppable'; import {Draggable} from './Draggable'; export function Example() { const [parent, setParent] = useState(); const draggable = ; return ( { const {target} = event.operation; if (event.canceled) return; setParent(target ? target.id : undefined); }} >
{parent == null ? draggable : null}
{parent === 'dropzone' ? draggable : null}
); } ================================================ FILE: apps/stories/stories/react/Resizeable/Resizeable.css ================================================ body, html, #root { height: 100%; } .Test { --max-width: 1600px; --gutter: 3vw; --grid-gutter: calc(var(--gutter, 4vw) - 11px); --cell-max-width: calc( ( var(--max-width, 1500px) - (11px * 23) ) / 24 ); --inset-padding: 0vw; --container-width: min(var(--max-width, 1500px), calc(100vw - var(--gutter, 4vw) * 2 - var(--inset-padding) )); grid-template-rows: repeat(20,minmax(calc(var(--container-width) * 0.0215), auto)); grid-template-columns: minmax(var(--grid-gutter), 1fr) repeat(24, minmax(0, var(--cell-max-width))) minmax(var(--grid-gutter), 1fr); } .Grid { display: grid; position: relative; grid-area: 1/1/-1/-1; --max-width: 1600px; --gap: 11px; --gutter: 3vw; --grid-gutter: calc(var(--gutter, 4vw) - 11px); --cell-max-width: calc( ( var(--max-width, 1500px) - (11px * 23) ) / 24 ); --inset-padding: 0vw; --container-width: min(var(--max-width, 1500px), calc(100vw - var(--gutter, 4vw) * 2 - var(--inset-padding) )); --column-width: var(--cell-max-width); --column-height: calc(var(--container-width) * 0.0215); grid-template-rows: repeat(20,minmax(var(--column-height), auto)); grid-template-columns: repeat(24, minmax(0, var(--column-width))); row-gap: var(--gap); column-gap: var(--gap); } .reveal::before { --bg-width: calc(var(--column-width) + var(--gap)); --bg-height: calc(var(--column-height) + var(--gap)); --border-color: #EEE; --gap-color: #EEE; --cell-color: transparent; content: ''; position: absolute; inset: 0; pointer-events: none; background-image: repeating-linear-gradient( -90deg, var(--gap-color) 0px, var(--gap-color) calc(var(--gap) - 1px), var(--border-color) var(--gap), var(--cell-color) calc(var(--gap)), var(--cell-color) calc(var(--bg-width) - 1px), var(--border-color) var(--bg-width) ), repeating-linear-gradient( 0deg, var(--gap-color) 0px, var(--gap-color) calc(var(--gap) - 1px), var(--border-color) var(--gap), var(--cell-color) var(--gap), var(--cell-color) calc(var(--bg-height) - 1px), var(--border-color) var(--bg-height) ); background-size: var(--bg-width) var(--bg-height); } .GridItem { position: relative; padding: 20px; // grid-area: attr(data-position); } .GridItem:hover, .selected { outline: 2px solid blue; } .Handle { position: absolute; padding: 10px; display: flex; } .Handle::after { content: ''; width: 10px; height: 10px; line-height: 0; margin: 0; border-radius: 3px; border: 2px solid blue; background-color: white; box-sizing: border-box; } .Handle[data-direction="N"] { top: -1px; left: 50%; transform: translate3d(-50%, -50%, 0); cursor: row-resize; } .Handle[data-direction="NW"] { top: -1px; left: -1px; transform: translate3d(-50%, -50%, 0); cursor: nw-resize; } .Handle[data-direction="NE"] { top: -1px; right: -1px; transform: translate3d(50%, -50%, 0); cursor: ne-resize; } .Handle[data-direction="S"] { bottom: -1px; left: 50%; transform: translate3d(-50%, 50%, 0); cursor: row-resize; } .Handle[data-direction="SW"] { bottom: -1px; left: -1px; transform: translate3d(-50%, 50%, 0); cursor: sw-resize; } .Handle[data-direction="W"] { top: 50%; left: -1px; transform: translate3d(-50%, -50%, 0); cursor: col-resize; } .Handle[data-direction="SE"] { bottom: -1px; right: -1px; transform: translate3d(50%, 50%, 0); cursor: se-resize; } .Handle[data-direction="E"] { top: 50%; right: -1px; transform: translate3d(50%, -50%, 0); cursor: col-resize; } ================================================ FILE: apps/stories/stories/react/Resizeable/Resizeable.tsx ================================================ import React, { useCallback, useEffect, useLayoutEffect, useState, useReducer, useRef, } from 'react'; import type {PropsWithChildren} from 'react'; import type {UniqueIdentifier} from '@dnd-kit/abstract'; import {DragDropProvider, useDraggable} from '@dnd-kit/react'; import './Resizeable.css'; import {Coordinates} from '@dnd-kit/geometry'; interface Layout { /** @x integer that represents the number of grid cells from the left edge */ x: number; /** @y integer that represents the number of grid cells from the top edge */ y: number; /** @z integer that represents the stacking order of the item */ z: number; /** @width Integer that represents the number of grid cells representing the width of the item */ width: number; /** @height Integer that represents the number of grid cells representing the height of the item */ height: number; } interface State { layouts: Record; } interface GridArea { columnStart: number; columnEnd: number; rowStart: number; rowEnd: number; } function toGridArea({x, y, width, height}: Layout): GridArea { return { columnStart: x, columnEnd: x + width, rowStart: y, rowEnd: y + height, }; } function toString({rowStart, columnStart, rowEnd, columnEnd}: GridArea) { return `${rowStart}/${columnEnd}/${rowEnd}/${columnStart}`; } const CELL_WIDTH = 56; const ROW_HEIGHT = 35; type Action = | { type: 'move'; id: UniqueIdentifier; data: Coordinates; } | { type: 'resize'; id: UniqueIdentifier; data: ResizeEvent; }; function sanitize(layout: Layout): Layout { const {x, y, z, width, height} = layout; return { ...layout, x: Math.min(Math.max(x, 1), CELL_WIDTH - (x + width)), y: Math.max(y, 1), z: Math.max(z, 0), width, height, }; } function move(layout: Layout, coordinates: Coordinates): Layout { const {x, y} = coordinates; return sanitize({ ...layout, x: layout.x + x, y: layout.y + y, }); } function resize(layout: Layout, data: ResizeEvent): Layout { const {x, y, width, height} = data; return sanitize({ ...layout, width: layout.width + width, height: layout.height + height, x: layout.x + x, y: layout.y + y, }); } function reducer(state: State, action: Action): State { switch (action.type) { case 'move': { const {id, data} = action; return { ...state, layouts: { ...state.layouts, [id]: move(state.layouts[id], data), }, }; } case 'resize': { const {id, data} = action; return { ...state, layouts: { ...state.layouts, [id]: resize(state.layouts[id], data), }, }; } } return state; } export function App() { const [selectedIds, setSelectedIds] = useState( null ); const [draggingIds, setDraggingIds] = useState( null ); const isDragging = draggingIds != null; const [state, dispatch] = useReducer(reducer, { layouts: { A: { x: 2, y: 3, z: 0, width: 6, height: 3, }, B: { x: 3, y: 10, z: 0, width: 4, height: 2, }, }, }); const {layouts} = state; const handleMove = useCallback( (id: UniqueIdentifier, coordinates: Coordinates) => { dispatch({type: 'move', id, data: coordinates}); }, [] ); const handleResize = useCallback( (id: UniqueIdentifier, data: ResizeEvent) => { dispatch({type: 'resize', id, data}); }, [] ); return ( { if (selectedIds?.length) { manager.actions.initalize(selectedIds); } setDraggingIds(manager.dragOperation.selectedIds); setSelectedIds(manager.dragOperation.selectedIds); }} onDragEnd={() => setDraggingIds(null)} > setSelectedIds(null)}> {Object.entries(layouts).map(([id, layout]) => ( setSelectedIds((ids) => (shiftKey ? [id, ...(ids ?? [])] : [id])) } dragging={draggingIds?.includes(id)} selected={selectedIds?.includes(id)} > Lorem ipsum dolor sit amet ))} ); } interface GridProps { reveal?: boolean; onClick?(): void; } function Grid({children, onClick, reveal}: PropsWithChildren) { const className = classNames('Grid', reveal && 'reveal'); return (
{children}
); } interface GridItemProps { id: UniqueIdentifier; layout: Layout; selected?: boolean; dragging?: boolean; onMove(id: UniqueIdentifier, coordinates: Coordinates): void; onSelect(id: UniqueIdentifier, shiftKey: boolean): void; onResize(id: UniqueIdentifier, event: ResizeEvent): void; } function GridItem({ id, layout, children, dragging, selected, onMove, onSelect, onResize, }: PropsWithChildren) { const [ref, setRef] = useState(null); const {transform} = useDraggable({ handle: ref, element: null, id, }); const x = transform ? Math.ceil(transform.x / CELL_WIDTH) : null; const y = transform ? Math.ceil(transform.y / ROW_HEIGHT) : null; const previous = useRef({x, y}); const showResizeControls = selected && !dragging; useLayoutEffect(() => { if ( x === null || y === null || (x === previous.current.x && y === previous.current.y) ) { previous.current = {x, y}; return; } const previousX = previous.current.x ?? 0; const previousY = previous.current.y ?? 0; onMove(id, {x: x - previousX, y: y - previousY}); previous.current = {x, y}; }, [x, y]); const className = classNames('GridItem', selected && 'selected'); return (
{ event.stopPropagation(); onSelect(id, event.shiftKey); }} > {children} onResize(id, event)} />
); } interface ResizeEvent { x: number; y: number; width: number; height: number; } interface ResizeableProps { disabled?: boolean; onResize(event: ResizeEvent): void; } const DIRECTIONS: Direction[] = ['NE', 'N', 'NW', 'E', 'W', 'SE', 'S', 'SW']; export function Resizeable({disabled, onResize}: ResizeableProps) { const handles = disabled ? null : ( <> {DIRECTIONS.map((direction) => ( ))} ); return {handles}; } type Direction = 'NE' | 'N' | 'NW' | 'SE' | 'S' | 'SW' | 'E' | 'W'; interface HandleProps { direction: Direction; onResize(event: ResizeEvent): void; } function Handle({direction, onResize}: HandleProps) { const ref = useRef(null); const {transform} = useDraggable({ handle: ref, element: null, id: direction, type: 'handle', }); const previousTransform = useRef(transform); const previous = useRef({x: 0, y: 0}); useEffect(() => { if ( JSON.stringify(transform) === JSON.stringify(previousTransform.current) ) { return; } previousTransform.current = transform; if (!transform) { return; } const x = Math.ceil(transform.x / CELL_WIDTH) - previous.current.x; const y = Math.ceil(transform.y / ROW_HEIGHT) - previous.current.y; previous.current.x = x; previous.current.y = x; const delta = { width: x, height: y, x: 0, y: 0, }; if (direction.includes('N')) { delta.y = y; delta.height = -1 * y; } if (direction.includes('W')) { delta.x = x; delta.width = -1 * x; } switch (direction) { case 'N': case 'S': delta.width = 0; break; case 'E': case 'W': delta.height = 0; break; } onResize(delta); }, [transform, onResize, direction]); return ; } function classNames(...classes: (string | boolean | undefined | null)[]) { return classes.filter(Boolean).join(' '); } ================================================ FILE: apps/stories/stories/react/Resizeable/index.ts ================================================ export {App} from './Resizeable'; ================================================ FILE: apps/stories/stories/react/Sortable/CSSLayers/CSSLayers.stories.tsx ================================================ import type {Meta, StoryObj} from '@storybook/react-vite'; import {CSSLayersExample} from './CSSLayersExample'; const meta: Meta = { title: 'React/Sortable/CSS Layers', component: CSSLayersExample, tags: ['hidden'], }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', }; ================================================ FILE: apps/stories/stories/react/Sortable/CSSLayers/CSSLayersExample.tsx ================================================ import React, {useState, memo, useEffect} from 'react'; import type {PropsWithChildren} from 'react'; import type {UniqueIdentifier} from '@dnd-kit/abstract'; import {DragDropProvider} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {move} from '@dnd-kit/helpers'; const LAYER_STYLES = ` @layer base, components; @layer base { .test { appearance: none; padding: 12px 20px; border: 2px solid rgb(76, 159, 254); border-radius: 8px; background: rgb(232, 240, 254); color: rgb(26, 58, 92); font-size: 15px; cursor: grab; white-space: nowrap; outline: none; } } @layer components { .test[data-shadow="true"] { opacity: 0.6; } } `; export function CSSLayersExample() { const [items, setItems] = useState([1, 2, 3, 4, 5]); useEffect(() => { const style = document.createElement('style'); style.setAttribute('data-test-layers', ''); style.textContent = LAYER_STYLES; document.head.appendChild(style); return () => { style.remove(); }; }, []); return ( { setItems((items) => move(items, event)); }} >
{items.map((id, index) => ( ))}
); } const LayerItem = memo(function LayerItem({ id, index, }: PropsWithChildren<{id: UniqueIdentifier; index: number}>) { const [element, setElement] = useState(null); const {isDragging} = useSortable({id, index, element}); return (
Item {id}
); }); ================================================ FILE: apps/stories/stories/react/Sortable/Grid/Grid.stories.tsx ================================================ import type {Meta, StoryObj} from '@storybook/react-vite'; import {pointerIntersection} from '@dnd-kit/collision'; import {Feedback} from '@dnd-kit/dom'; import {SortableExample} from '../SortableExample'; import gridSortableSource from './GridSortableApp.tsx?raw'; import {baseStyles, sortableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'React/Sortable/Grid', component: SortableExample, }; export default meta; type Story = StoryObj; const defaultArgs = { debug: false, layout: 'grid', } as const; export const Grid: Story = { name: 'Basic setup', args: defaultArgs, parameters: { codesandbox: { files: { 'src/App.tsx': gridSortableSource, 'src/styles.css': [baseStyles, sortableStyles].join('\n\n'), }, }, }, }; export const DragHandle: Story = { name: 'Drag handle', args: { ...defaultArgs, dragHandle: true, }, }; export const VariableSizes: Story = { name: 'Variable sizes', args: { ...defaultArgs, itemCount: 14, collisionDetector: pointerIntersection, optimistic: false, getItemStyle(_: number, index: number) { if (index === 0 || index === 10) { return { maxWidth: 'initial', gridRowStart: 'span 2', gridColumnStart: 'span 2', }; } }, }, }; export const Clone: Story = { name: 'Clone feedback', args: { ...defaultArgs, collisionDetector: pointerIntersection, optimistic: false, getItemStyle(_, index) { if (index === 0 || index === 2 || index === 10) { return { maxWidth: 'initial', gridRowStart: 'span 2', gridColumnStart: 'span 2', }; } if (index === 12) { return { gridRowStart: 'span 2', }; } }, plugins: [Feedback.configure({feedback: 'clone'})], }, }; export const Debug: Story = { name: 'Debug', args: { ...defaultArgs, debug: true, }, }; ================================================ FILE: apps/stories/stories/react/Sortable/Grid/GridSortableApp.tsx ================================================ import React, {useState} from 'react'; import {DragDropProvider} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {move} from '@dnd-kit/helpers'; function Sortable({id, index}: {id: number; index: number}) { const [element, setElement] = useState(null); const {isDragging} = useSortable({id, index, element}); return (
{id}
); } export default function App() { const [items, setItems] = useState(createRange(20)); return ( { setItems((items) => move(items, event)); }} >
{items.map((id, index) => ( ))}
); } function createRange(length: number) { return Array.from({length}, (_, i) => i + 1); } ================================================ FILE: apps/stories/stories/react/Sortable/Horizontal/Horizontal.stories.tsx ================================================ import type {Meta, StoryObj} from '@storybook/react-vite'; import {RestrictToHorizontalAxis} from '@dnd-kit/abstract/modifiers'; import {Feedback} from '@dnd-kit/dom'; import {SortableExample} from '../SortableExample'; import horizontalSortableSource from './HorizontalSortableApp.tsx?raw'; import {baseStyles, sortableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'React/Sortable/Horizontal list', component: SortableExample, }; export default meta; type Story = StoryObj; const defaultArgs = { debug: false, layout: 'horizontal', getItemStyle() { return {width: 180}; }, } as const; export const Horizontal: Story = { name: 'Basic setup', args: defaultArgs, parameters: { codesandbox: { files: { 'src/App.tsx': horizontalSortableSource, 'src/styles.css': [baseStyles, sortableStyles].join('\n\n'), }, }, }, }; export const DragHandle: Story = { name: 'Drag handle', args: { ...defaultArgs, dragHandle: true, }, }; export const VariableWidths: Story = { name: 'Variable widths', args: { ...defaultArgs, getItemStyle(id) { const widths = {0: 140, 2: 120, 4: 140, 5: 240, 8: 100, 12: 150}; return { width: widths[id] ?? 180, }; }, }, }; export const Clone: Story = { name: 'Clone feedback', args: { ...defaultArgs, plugins: [Feedback.configure({feedback: 'clone'})], }, }; export const HorizontalAxis: Story = { name: 'Restrict axis', args: { ...defaultArgs, modifiers: [RestrictToHorizontalAxis], }, }; export const Debug: Story = { name: 'Debug', args: { ...defaultArgs, debug: true, }, }; ================================================ FILE: apps/stories/stories/react/Sortable/Horizontal/HorizontalSortableApp.tsx ================================================ import React, {useState} from 'react'; import {DragDropProvider} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {move} from '@dnd-kit/helpers'; function Sortable({id, index}: {id: number; index: number}) { const [element, setElement] = useState(null); const {isDragging} = useSortable({id, index, element}); return (
{id}
); } export default function App() { const [items, setItems] = useState(createRange(10)); return ( { setItems((items) => move(items, event)); }} >
{items.map((id, index) => ( ))}
); } function createRange(length: number) { return Array.from({length}, (_, i) => i + 1); } ================================================ FILE: apps/stories/stories/react/Sortable/Iframe/Iframe.stories.tsx ================================================ import type {Meta, StoryObj} from '@storybook/react-vite'; import {IframeLists} from './IframeExample.tsx'; const meta: Meta = { title: 'React/Sortable/Iframe', component: IframeLists, }; export default meta; type Story = StoryObj; export const Iframe: Story = { name: 'Iframe', args: { debug: false, itemCount: 6, }, }; export const IframeTransformed: Story = { name: 'Transformed Iframe', args: { debug: false, itemCount: 6, transform: true, }, }; ================================================ FILE: apps/stories/stories/react/Sortable/Iframe/IframeExample.tsx ================================================ import React, {useEffect, useRef, useState} from 'react'; import type {PropsWithChildren} from 'react'; import {DragDropProvider} from '@dnd-kit/react'; import {DragOverlay} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {move} from '@dnd-kit/helpers'; import {Debug} from '@dnd-kit/dom/plugins/debug'; import AutoFrameComponent from '@measured/auto-frame-component'; import {Container, Item} from '../../components/index.ts'; import {createRange} from '@dnd-kit/stories-shared/utilities'; const AutoFrame = AutoFrameComponent.default || AutoFrameComponent; interface Props { debug?: boolean; defaultItems?: Record; columnStyle?: Record; itemCount: number; scrollable?: boolean; transform?: boolean; } export function IframeLists({ debug, defaultItems, itemCount, columnStyle, scrollable, transform, }: Props) { const [items, setItems] = useState( defaultItems ?? { host: createRange(itemCount).map((id) => `Host: ${id}`), iframe: createRange(itemCount).map((id) => `Iframe: ${id}`), } ); const snapshot = useRef(structuredClone(items)); const [bodyClassName, setBodyClassName] = useState(''); useEffect(() => { const body = document.querySelector('body'); if (!body) return; if (body.classList.contains('dark')) { setBodyClassName('dark'); } }, []); return ( [...defaults, Debug] : undefined} onDragStart={() => { snapshot.current = structuredClone(items); }} onDragOver={(event) => { setItems((items) => move(items, event)); }} onDragEnd={(event) => { if (event.canceled) { setItems(snapshot.current); return; } }} >
{items.host.map((id, index) => ( ))} : null}
{columns.map((column, columnIndex) => { const rows = items[column]; return ( ); })}
); } interface SortableItemProps { id: string; column: string; index: number; style?: React.CSSProperties; onRemove?: (id: string, column: string) => void; } const COLORS: Record = { A: '#7193f1', B: '#FF851B', C: '#2ECC40', D: '#ff3680', }; const SortableItem = memo(function SortableItem({ id, column, index, style, onRemove, }: PropsWithChildren) { const group = column; const {handleRef, ref, isDragging} = useSortable({ id, group, accept: 'item', type: 'item', plugins: [Feedback.configure({feedback: 'clone'})], index, data: {group}, }); return ( {onRemove && !isDragging ? ( onRemove(id, column)} /> ) : null} } accentColor={COLORS[column]} shadow={isDragging} style={style} transitionId={`sortable-${column}-${id}`} > {id} ); }); interface SortableColumnProps { columns: number; id: string; index: number; scrollable?: boolean; style?: React.CSSProperties; rows: string[]; onRemove?: SortableItemProps['onRemove']; } const SortableColumn = memo(function SortableColumn({ rows, columns, id, index, scrollable, style, onRemove, }: PropsWithChildren) { const {handleRef, isDragging, ref} = useSortable({ id, accept: ['column', 'item'], collisionPriority: CollisionPriority.Low, type: 'column', index, }); const actions = useMemo(() => { return ( ); }, [handleRef]); const handleRemoveItem = useCallback( (itemId: string) => { onRemove?.(itemId, id); }, [id, onRemove] ); return ( {rows.map((itemId, index) => ( ))} ); }); ================================================ FILE: apps/stories/stories/react/Sortable/MultipleLists/MultipleListsApp.tsx ================================================ import React, {memo, useCallback, useRef, useState} from 'react'; import type {PropsWithChildren} from 'react'; import {CollisionPriority} from '@dnd-kit/abstract'; import {DragDropProvider} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {move} from '@dnd-kit/helpers'; import {Feedback, PointerSensor, KeyboardSensor} from '@dnd-kit/dom'; import {DragDropEventHandlers} from '@dnd-kit/react'; function createRange(length: number) { return Array.from({length}, (_, i) => i + 1); } const ITEM_COUNT = 6; const sensors = [ PointerSensor.configure({ activatorElements(source) { return [source.element, source.handle]; }, }), KeyboardSensor, ]; interface SortableItemProps { id: string; column: string; index: number; accentColor: string; } const COLORS: Record = { A: '#7193f1', B: '#FF851B', C: '#2ECC40', D: '#ff3680', }; const SortableItem = memo(function SortableItem({ id, column, index, accentColor, }: PropsWithChildren) { const group = column; const {handleRef, ref, isDragging} = useSortable({ id, group, accept: 'item', type: 'item', plugins: [Feedback.configure({feedback: 'clone'})], index, data: {group}, }); return (
{id}
); }); interface SortableColumnProps { id: string; index: number; rows: string[]; } const SortableColumn = memo(function SortableColumn({ rows, id, index, }: PropsWithChildren) { const {handleRef, isDragging, ref} = useSortable({ id, accept: ['column', 'item'], collisionPriority: CollisionPriority.Low, type: 'column', index, }); return (

{id}

    {rows.map((itemId, itemIndex) => ( ))}
); }); export default function App() { const [items, setItems] = useState({ A: createRange(ITEM_COUNT).map((id) => `A${id}`), B: createRange(ITEM_COUNT).map((id) => `B${id}`), C: createRange(ITEM_COUNT).map((id) => `C${id}`), D: [], }); const [columns] = useState(Object.keys(items)); const snapshot = useRef(structuredClone(items)); return ( (() => { snapshot.current = structuredClone(items); }, [items])} onDragOver={useCallback((event) => { const {source} = event.operation; if (source && source.type === 'column') { return; } setItems((items) => move(items, event)); }, [])} onDragEnd={useCallback((event) => { if (event.canceled) { setItems(snapshot.current); return; } }, [])} >
{columns.map((column, columnIndex) => { const rows = items[column as keyof typeof items]; return ( ); })}
); } ================================================ FILE: apps/stories/stories/react/Sortable/MultipleLists/docs/MultipleLists.mdx ================================================ import {Code, Preview} from '../../../../components'; import {MultipleLists} from '../MultipleLists.tsx'; import {Hero} from '../MultipleLists.stories.tsx'; import {Example} from './examples/QuickStart'; import ExampleSource from './examples/QuickStart?raw'; import ColumnSource from './examples/Column?raw'; import ItemSource from './examples/Item?raw'; # Multiple lists Reorder sortable elements across multiple lists. ## Installation Before getting started, make sure to install the `@dnd-kit/react` package: ```bash npm install @dnd-kit/react ``` ## Getting started ================================================ FILE: apps/stories/stories/react/Sortable/MultipleLists/docs/examples/Column.jsx ================================================ import React from 'react'; import {useDroppable} from '@dnd-kit/react'; import {CollisionPriority} from '@dnd-kit/abstract'; const styles = { display: 'flex', flexDirection: 'column', gap: 10, padding: 20, minWidth: 200, backgroundColor: 'rgba(0,0,0,0.1)', borderRadius: 10, }; export function Column({children, id}) { const {ref} = useDroppable({ id, type: 'column', accept: ['item'], collisionPriority: CollisionPriority.Low, }); return (
{children}
); } ================================================ FILE: apps/stories/stories/react/Sortable/MultipleLists/docs/examples/Item.jsx ================================================ import React from 'react'; import {useSortable} from '@dnd-kit/react/sortable'; export function Item({id, column, index}) { const {ref} = useSortable({ id, index, group: column, type: 'item', accept: ['item'], }); return ; } ================================================ FILE: apps/stories/stories/react/Sortable/MultipleLists/docs/examples/QuickStart.jsx ================================================ import React, {useState} from 'react'; import {DragDropProvider} from '@dnd-kit/react'; import {move} from '@dnd-kit/helpers'; import {Column} from './Column'; import {Item} from './Item'; const styles = {display: 'inline-flex', flexDirection: 'row', gap: 20}; export function Example({style = styles}) { const [items, setItems] = useState({ A: ['A0', 'A1', 'A2'], B: ['B0', 'B1'], C: [], }); return ( { setItems((items) => move(items, event)); }} >
{Object.entries(items).map(([column, items]) => ( {items.map((id, index) => ( ))} ))}
); } ================================================ FILE: apps/stories/stories/react/Sortable/Quickstart.tsx ================================================ import {useSortable} from '@dnd-kit/react/sortable'; export function Sortable({id, index}: {id: number; index: number}) { const {ref} = useSortable({id, index}); return
  • Item {id}
  • ; } export function QuickstartExample() { const items = [1, 2, 3, 4]; return (
      {items.map((id, index) => ( ))}
    ); } ================================================ FILE: apps/stories/stories/react/Sortable/Sortable.stories.tsx ================================================ import type {Meta, StoryObj} from '@storybook/react-vite'; import SortableApp from './SortableApp.tsx'; import sortableSource from './SortableApp.tsx?raw'; import {baseStyles, sortableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; import {QuickstartExample} from './Quickstart.tsx'; import docs from './docs/SortableDocs.mdx'; const meta: Meta = { component: SortableApp, title: 'React/Sortable', tags: ['autodocs'], parameters: { docs: { page: docs, }, codesandbox: { files: { 'src/App.tsx': sortableSource, 'src/styles.css': [baseStyles, sortableStyles].join('\n\n'), }, }, }, }; export default meta; type Story = StoryObj; export const Example: Story = { name: 'Example', }; export const Quickstart: Story = { name: 'Quickstart', tags: ['hidden'], render: () => , }; ================================================ FILE: apps/stories/stories/react/Sortable/SortableApp.tsx ================================================ import React, {useRef, useState} from 'react'; import {DragDropProvider} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {move} from '@dnd-kit/helpers'; function Sortable({id, index}: {id: number; index: number}) { const [element, setElement] = useState(null); const handleRef = useRef(null); const {isDragging} = useSortable({id, index, element, handle: handleRef}); return (
  • {id}
  • ); } export default function App() { const [items, setItems] = useState(createRange(100)); return ( { setItems((items) => move(items, event)); }} >
      {items.map((id, index) => ( ))}
    ); } function createRange(length: number) { return Array.from({length}, (_, i) => i + 1); } ================================================ FILE: apps/stories/stories/react/Sortable/SortableExample.tsx ================================================ import React, {useRef, useState, memo} from 'react'; import type {CSSProperties, PropsWithChildren} from 'react'; import type { CollisionDetector, Modifiers, Plugins, UniqueIdentifier, } from '@dnd-kit/abstract'; import {type SortableTransition} from '@dnd-kit/dom/sortable'; import {DragDropProvider} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {directionBiased} from '@dnd-kit/collision'; import {move} from '@dnd-kit/helpers'; import {Debug} from '@dnd-kit/dom/plugins/debug'; import {Item, Handle} from '../components/index.ts'; import {createRange} from '@dnd-kit/stories-shared/utilities'; interface Props { debug?: boolean; dragHandle?: boolean; disabled?: UniqueIdentifier[]; plugins?: Plugins; modifiers?: Modifiers; layout?: 'vertical' | 'horizontal' | 'grid'; transition?: SortableTransition; itemCount?: number; optimistic?: boolean; collisionDetector?: CollisionDetector; getItemStyle?(id: UniqueIdentifier, index: number): CSSProperties; } export function SortableExample({ debug, itemCount = 15, collisionDetector, disabled, dragHandle, plugins, layout = 'vertical', optimistic = true, modifiers, transition, getItemStyle, }: Props) { const [items, setItems] = useState(createRange(itemCount)); return ( [Debug, ...defaults] : undefined} modifiers={modifiers} onDragOver={(event) => { if (optimistic) return; setItems((items) => move(items, event)); }} onDragEnd={(event) => { setItems((items) => move(items, event)); }} > {items.map((id, index) => ( ))} ); } interface SortableProps { id: UniqueIdentifier; index: number; collisionDetector?: CollisionDetector; disabled?: boolean; dragHandle?: boolean; plugins?: Plugins; optimistic?: boolean; transition?: SortableTransition; style?: React.CSSProperties; } const SortableItem = memo(function SortableItem({ id, index, collisionDetector = directionBiased, disabled, dragHandle, plugins, transition, style, }: PropsWithChildren) { const [element, setElement] = useState(null); const handleRef = useRef(null); const {isDragging} = useSortable({ id, index, element, plugins, transition, handle: handleRef, disabled, collisionDetector, }); return ( : null} shadow={isDragging} style={style} > {id} ); }); function Wrapper({ layout, children, }: PropsWithChildren<{layout: 'vertical' | 'horizontal' | 'grid'}>) { return
    {children}
    ; } function getWrapperStyles( layout: 'vertical' | 'horizontal' | 'grid' ): CSSProperties { const baseStyles: CSSProperties = { gap: 18, padding: '0 30px', }; switch (layout) { case 'grid': return { ...baseStyles, display: 'grid', maxWidth: 900, marginInline: 'auto', gridTemplateColumns: 'repeat(auto-fill, 150px)', gridAutoFlow: 'dense', gridAutoRows: '150px', justifyContent: 'center', }; case 'horizontal': return { ...baseStyles, display: 'inline-flex', flexDirection: 'row', alignItems: 'stretch', height: 180, }; case 'vertical': return { ...baseStyles, display: 'flex', flexDirection: 'column', alignItems: 'center', }; } } ================================================ FILE: apps/stories/stories/react/Sortable/Table/Table.stories.tsx ================================================ import type {Meta, StoryObj} from '@storybook/react-vite'; import {TableExample} from './TableExample.tsx'; const meta: Meta = { title: 'React/Sortable/Table', component: TableExample, }; export default meta; type Story = StoryObj; export const Example: Story = { name: 'Example', }; ================================================ FILE: apps/stories/stories/react/Sortable/Table/TableExample.tsx ================================================ import React, {useRef, useState} from 'react'; import type {UniqueIdentifier} from '@dnd-kit/abstract'; import {RestrictToHorizontalAxis} from '@dnd-kit/abstract/modifiers'; import {DragDropProvider} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {move} from '@dnd-kit/helpers'; import {Handle} from '../../components/index.ts'; type ColumnKey = 'name' | 'role' | 'email' | 'status'; interface Column { id: ColumnKey; label: string; } const initialColumns: Column[] = [ {id: 'name', label: 'Name'}, {id: 'role', label: 'Role'}, {id: 'email', label: 'Email'}, {id: 'status', label: 'Status'}, ]; interface RowData { id: UniqueIdentifier; name: string; role: string; email: string; status: string; } const initialData: RowData[] = [ { id: 1, name: 'Alice Johnson', role: 'Engineer', email: 'alice@example.com', status: 'Active', }, { id: 2, name: 'Bob Smith', role: 'Designer', email: 'bob@example.com', status: 'Active', }, { id: 3, name: 'Charlie Brown', role: 'Manager', email: 'charlie@example.com', status: 'Away', }, { id: 4, name: 'Diana Ross', role: 'Engineer', email: 'diana@example.com', status: 'Active', }, { id: 5, name: 'Eve Williams', role: 'Designer', email: 'eve@example.com', status: 'Offline', }, { id: 6, name: 'Frank Miller', role: 'Engineer', email: 'frank@example.com', status: 'Active', }, { id: 7, name: 'Grace Lee', role: 'Manager', email: 'grace@example.com', status: 'Away', }, { id: 8, name: 'Hank Davis', role: 'Designer', email: 'hank@example.com', status: 'Active', }, ]; export function TableExample() { const [rows, setRows] = useState(initialData); const [columns, setColumns] = useState(initialColumns); const initialOrder = useRef({ columns, rows, }); return ( { initialOrder.current = { columns, rows, }; }} onDragOver={(event) => { const {source} = event.operation; if (source?.type === 'column') { setColumns((columns) => move(columns, event)); } else { setRows((rows) => move(rows, event)); } }} onDragEnd={(event) => { if (event.canceled) { setColumns(initialOrder.current.columns); setRows(initialOrder.current.rows); } }} >
    {rows.map((row, index) => ( ))}
    {columns.map((column, index) => ( ))}
    ); } interface SortableColumnProps { column: Column; index: number; } function SortableColumn({column, index}: SortableColumnProps) { const {ref, isDragging} = useSortable({ id: column.id, index, type: 'column', accept: 'column', modifiers: [RestrictToHorizontalAxis], }); return ( {column.label} ); } interface SortableRowProps { row: RowData; columns: Column[]; index: number; lastRow?: boolean; } function SortableRow({row, columns, index, lastRow}: SortableRowProps) { const {ref, handleRef, isDragging} = useSortable({ id: row.id, index, type: 'row', accept: 'row', }); return ( {columns.map((column) => ( {column.id === 'status' ? ( {row[column.id]} ) : ( row[column.id] )} ))} ); } const tableStyles: React.CSSProperties = { width: '100%', borderCollapse: 'separate', borderSpacing: 0, fontFamily: 'system-ui, sans-serif', fontSize: 14, }; const thStyles: React.CSSProperties = { textAlign: 'left', padding: '10px 16px', borderBottom: '2px solid #e2e8f0', color: '#64748b', fontWeight: 600, fontSize: 12, textTransform: 'uppercase', letterSpacing: '0.05em', }; const trStyles: React.CSSProperties = { backgroundColor: '#fff', }; const tdStyles: React.CSSProperties = { padding: '12px 16px', borderBottom: '1px solid #e2e8f0', }; const tdLastRowStyles: React.CSSProperties = { padding: '12px 16px', }; function getStatusStyles(status: string): React.CSSProperties { const colors: Record = { Active: {bg: '#dcfce7', text: '#166534'}, Away: {bg: '#fef9c3', text: '#854d0e'}, Offline: {bg: '#f1f5f9', text: '#475569'}, }; const {bg, text} = colors[status] ?? colors.Offline; return { padding: '2px 10px', borderRadius: 12, fontSize: 12, fontWeight: 500, backgroundColor: bg, color: text, }; } ================================================ FILE: apps/stories/stories/react/Sortable/Transformed/Transformed.stories.tsx ================================================ import type {Meta, StoryObj} from '@storybook/react-vite'; import {TransformedExample} from './TransformedExample'; const meta: Meta = { title: 'React/Sortable/Transformed', component: TransformedExample, }; export default meta; type Story = StoryObj; export const WithoutOverlay: Story = { name: 'Without overlay', args: { overlay: false, }, }; export const WithOverlay: Story = { name: 'With overlay', args: { overlay: true, }, }; ================================================ FILE: apps/stories/stories/react/Sortable/Transformed/TransformedExample.tsx ================================================ import React, {useState} from 'react'; import type {CSSProperties} from 'react'; import type {UniqueIdentifier} from '@dnd-kit/abstract'; import {DragDropProvider, DragOverlay} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {Debug} from '@dnd-kit/dom/plugins/debug'; import {move} from '@dnd-kit/helpers'; import {Item} from '../../components/index.ts'; import {createRange} from '@dnd-kit/stories-shared/utilities'; const ITEM_HEIGHT = 62; interface Props { debug?: boolean; overlay?: boolean; } /** * Sortable list where items are absolutely positioned using CSS * `transform: translateY()`. This mimics the positioning strategy used by * virtualization libraries like react-window v2 and is useful for testing * that @dnd-kit correctly handles elements with pre-existing CSS transforms. */ export function TransformedExample({debug, overlay}: Props) { const [items, setItems] = useState(createRange(10)); return ( [Debug, ...defaults] : undefined} onDragOver={(event) => { setItems((items) => move(items, event)); }} onDragEnd={(event) => { setItems((items) => move(items, event)); }} >
    {items.map((id, index) => ( ))}
    {overlay ? ( {(source) => ( {source.id} )} ) : null}
    ); } interface TransformedItemProps { id: UniqueIdentifier; index: number; overlay?: boolean; style: CSSProperties; } function TransformedItem({id, index, overlay, style}: TransformedItemProps) { const {isDragSource, isDragging, ref} = useSortable({ id, index, }); return ( } style={{ ...style, display: 'flex', justifyContent: 'center', alignItems: 'center', boxSizing: 'border-box', }} data-index={index} shadow={!overlay && isDragging} aria-hidden={overlay ? isDragSource : undefined} > {id} ); } ================================================ FILE: apps/stories/stories/react/Sortable/Tree/Tree.module.css ================================================ .Tree { max-width: 600px; padding: 10px; margin: 10% auto 0px; } .TreeItem { position: relative; display: flex; align-items: center; padding: 10px; gap: 10px; background-color: #fff; border: 1px solid #dedede; margin-bottom: -1px; color: #222; box-sizing: border-box; &:first-child { border-top-left-radius: 6px; border-top-right-radius: 6px; } &:last-child { border-bottom-left-radius: 6px; border-bottom-right-radius: 6px; } &[data-overlay] { width: max-content !important; padding-right: 24px; border-radius: 6px; box-shadow: 0px 15px 15px 0 rgba(34, 33, 81, 0.1); } } .TreeItem[aria-hidden="true"] { opacity: 0.4; .Handle { visibility: hidden; } } .Action { margin-left: auto; } .Badge { position: absolute; top: -10px; right: -10px; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; background-color: #2389ff; font-size: 0.8rem; font-weight: 500; color: #fff; } ================================================ FILE: apps/stories/stories/react/Sortable/Tree/Tree.stories.tsx ================================================ import React, {useState} from 'react'; import type {Meta, StoryObj} from '@storybook/react-vite'; import {Tree} from './Tree'; import type {Item} from './types'; const meta: Meta = { title: 'React/Sortable/Tree', component: Tree, }; export default meta; type Story = StoryObj; export const Example: Story = { name: 'Example', render: () => { const [items, setItems] = useState([ { id: 'Home', children: [], }, { id: 'Collections', children: [ {id: 'Spring', children: []}, {id: 'Summer', children: []}, {id: 'Fall', children: []}, {id: 'Winter', children: []}, ], }, { id: 'About Us', children: [], }, { id: 'My Account', children: [ {id: 'Addresses', children: []}, {id: 'Order History', children: []}, ], }, ]); return ; }, }; ================================================ FILE: apps/stories/stories/react/Sortable/Tree/Tree.tsx ================================================ import {useRef, useState} from 'react'; import {DragDropProvider, DragOverlay} from '@dnd-kit/react'; import {isKeyboardEvent} from '@dnd-kit/dom/utilities'; import {move} from '@dnd-kit/helpers'; import {FlattenedItem, type Item} from './types.js'; import { flattenTree, buildTree, getProjection, getDragDepth, getDescendants, } from './utilities.js'; import {TreeItem} from './TreeItem.js'; import {TreeItemOverlay} from './TreeItemOverlay.js'; import styles from './Tree.module.css'; interface Props { items: Item[]; indentation?: number; onChange(items: Item[]): void; } export function Tree({items, indentation = 50, onChange}: Props) { const [flattenedItems, setFlattenedItems] = useState(() => flattenTree(items) ); const initialDepth = useRef(0); const sourceChildren = useRef([]); return ( { const {source} = event.operation; if (!source) return; const {depth} = flattenedItems.find(({id}) => id === source.id)!; // Store the source item's initial depth for later use initialDepth.current = depth; setFlattenedItems((flattenedItems) => { sourceChildren.current = []; // Get all descendants of the source item const descendants = getDescendants(flattenedItems, source.id); return flattenedItems.filter((item) => { if (descendants.has(item.id)) { sourceChildren.current = [...sourceChildren.current, item]; return false; } return true; }); }); initialDepth.current = depth; }} onDragOver={(event, manager) => { const {source, target} = event.operation; event.preventDefault(); if (source && target && source.id !== target.id) { setFlattenedItems((flattenedItems) => { const offsetLeft = manager.dragOperation.transform.x; const dragDepth = getDragDepth(offsetLeft, indentation); const projectedDepth = initialDepth.current + dragDepth; const {depth, parentId} = getProjection( flattenedItems, target.id, projectedDepth ); const sortedItems = move(flattenedItems, event); const newItems = sortedItems.map((item) => item.id === source.id ? {...item, depth, parentId} : item ); return newItems; }); } }} onDragMove={(event, manager) => { if (event.defaultPrevented) { return; } const {source, target} = event.operation; if (source && target) { const keyboard = isKeyboardEvent(event.operation.activatorEvent); const currentDepth = source.data!.depth ?? 0; let keyboardDepth; if (keyboard) { const isHorizontal = event.by?.x !== 0 && event.by?.y === 0; if (isHorizontal) { event.preventDefault(); keyboardDepth = currentDepth + Math.sign(event.by!.x); } } const offsetLeft = manager.dragOperation.transform.x; const dragDepth = getDragDepth(offsetLeft, indentation); const projectedDepth = keyboardDepth ?? initialDepth.current + dragDepth; const {depth, parentId} = getProjection( flattenedItems, source.id, projectedDepth ); if (keyboard) { if (currentDepth !== depth) { const offset = indentation * (depth - currentDepth); manager.actions.move({ by: {x: offset, y: 0}, propagate: false, }); } } if ( source.data!.depth !== depth || source.data!.parentId !== parentId ) { setFlattenedItems((flattenedItems) => { return flattenedItems.map((item) => item.id === source.id ? {...item, depth, parentId} : item ); }); } } }} onDragEnd={(event) => { if (event.canceled) { return setFlattenedItems(flattenTree(items)); } const updatedTree = buildTree([ ...flattenedItems, ...sourceChildren.current, ]); setFlattenedItems(flattenTree(updatedTree)); onChange(updatedTree); }} >
      {flattenedItems.map((item, index) => ( { const newItems = flattenedItems.filter(({id}) => id !== item.id); const tree = buildTree(newItems); setFlattenedItems(flattenTree(tree)); onChange(tree); }} /> ))}
    {(source) => ( )}
    ); } ================================================ FILE: apps/stories/stories/react/Sortable/Tree/TreeItem.tsx ================================================ import {useSortable} from '@dnd-kit/react/sortable'; import {Handle} from '../../components/Handle/Handle.tsx'; import {Remove} from '../../components/Actions/Remove.tsx'; import {FlattenedItem} from './types.ts'; import styles from './Tree.module.css'; export interface Props extends FlattenedItem { onRemove?(): void; } const INDENTATION = 50; const config = { alignment: { x: 'start', y: 'center', }, transition: { idle: true, }, } as const; export function TreeItem({depth, id, index, parentId, onRemove}: Props) { const {ref, handleRef, isDragSource} = useSortable({ ...config, id, index, data: { depth, parentId, }, }); return (
  • {id} {onRemove && (
    )}
  • ); } ================================================ FILE: apps/stories/stories/react/Sortable/Tree/TreeItemOverlay.tsx ================================================ import type {UniqueIdentifier} from '@dnd-kit/abstract'; import {Handle} from '../../components/Handle/Handle.tsx'; import styles from './Tree.module.css'; interface Props { id: UniqueIdentifier; count: number; } export function TreeItemOverlay({id, count}: Props) { return (
    {id} {count > 0 ? {count} : null}
    ); } ================================================ FILE: apps/stories/stories/react/Sortable/Tree/types.ts ================================================ export interface Item { id: string; children: Item[]; collapsed?: boolean; } export interface FlattenedItem extends Item { parentId: string | null; depth: number; index: number; } ================================================ FILE: apps/stories/stories/react/Sortable/Tree/utilities.ts ================================================ import {UniqueIdentifier} from '@dnd-kit/abstract'; import type {Item, FlattenedItem} from './types.ts'; export function flattenTree( items: Item[], parentId: string | null = null, depth = 0 ): FlattenedItem[] { return items.reduce((acc, item, index) => { return [ ...acc, {...item, parentId, depth, index}, ...flattenTree(item.children, item.id, depth + 1), ]; }, []); } export function buildTree(flattenedItems: FlattenedItem[]): Item[] { const root: Item = {id: 'root', children: []}; const nodes: Record = {[root.id]: root}; const items = flattenedItems.map((item) => ({...item, children: []})); for (const item of items) { const {id, children} = item; const parentId = item.parentId ?? root.id; const parent = nodes[parentId] ?? items.find(({id}) => id === parentId); if (!parent) continue; nodes[id] = {id, children}; parent.children.push(item); } return root.children; } export function getDragDepth(offset: number, indentationWidth: number) { return Math.round(offset / indentationWidth); } export function getProjection( items: FlattenedItem[], targetId: UniqueIdentifier, projectedDepth: number ) { const targetItemIndex = items.findIndex(({id}) => id === targetId); const previousItem = items[targetItemIndex - 1]; const targetItem = items[targetItemIndex]; const nextItem = items[targetItemIndex + 1]; const maxDepth = getMaxDepth(targetItem, previousItem); const minDepth = getMinDepth(nextItem); let depth = projectedDepth; if (projectedDepth >= maxDepth) { depth = maxDepth; } else if (projectedDepth < minDepth) { depth = minDepth; } return {depth, maxDepth, minDepth, parentId: getParentId()}; function getParentId() { if (depth === 0 || !previousItem) { return null; } if (depth === previousItem.depth) { return previousItem.parentId; } if (depth > previousItem.depth) { return previousItem.id; } const newParent = items .slice(0, targetItemIndex) .reverse() .find((item) => item.depth === depth)?.parentId; return newParent ?? null; } } function getMaxDepth( targetItem: FlattenedItem, previousItem: FlattenedItem | undefined ) { if (!previousItem) return 0; return Math.min(targetItem.depth + 1, previousItem.depth + 1); } function getMinDepth(nextItem: FlattenedItem) { return nextItem ? nextItem.depth : 0; } export function getDescendants( items: FlattenedItem[], parentId: UniqueIdentifier ): Set { const directChildren = items.filter((item) => item.parentId === parentId); return directChildren.reduce((descendants, child) => { return new Set([ ...descendants, child.id, ...getDescendants(items, child.id), ]); }, new Set()); } ================================================ FILE: apps/stories/stories/react/Sortable/Vertical/AutoScrollExample.tsx ================================================ import React, {useState, memo} from 'react'; import type {PropsWithChildren} from 'react'; import type {Axis} from '@dnd-kit/geometry'; import {AutoScroller} from '@dnd-kit/dom'; import {DragDropProvider} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {move} from '@dnd-kit/helpers'; import {directionBiased} from '@dnd-kit/collision'; import {Item} from '../../components/index.ts'; import {createRange} from '@dnd-kit/stories-shared/utilities'; interface Props { itemCount?: number; acceleration?: number; threshold?: number | Record; } export function AutoScrollExample({ itemCount = 100, acceleration, threshold, }: Props) { const [items, setItems] = useState(createRange(itemCount)); const plugins = acceleration != null || threshold != null ? (defaults: any) => [ ...defaults, AutoScroller.configure({acceleration, threshold}), ] : undefined; return ( { setItems((items) => move(items, event)); }} >
    {items.map((id, index) => ( ))}
    ); } const SortableItem = memo(function SortableItem({ id, index, }: PropsWithChildren<{id: number; index: number}>) { const [element, setElement] = useState(null); const {isDragging} = useSortable({ id, index, element, collisionDetector: directionBiased, }); return ( {id} ); }); ================================================ FILE: apps/stories/stories/react/Sortable/Vertical/Vertical.stories.tsx ================================================ import type {Meta, StoryObj} from '@storybook/react-vite'; import {RestrictToVerticalAxis} from '@dnd-kit/abstract/modifiers'; import {Feedback} from '@dnd-kit/dom'; import {SortableExample} from '../SortableExample'; import {AutoScrollExample} from './AutoScrollExample'; const meta: Meta = { title: 'React/Sortable/Vertical list', component: SortableExample, }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', args: { debug: false, itemCount: 100, }, }; export const WithDragHandle: Story = { name: 'Drag handle', args: { debug: false, dragHandle: true, itemCount: 100, }, }; export const VariableHeights: Story = { name: 'Variable heights', args: { debug: false, getItemStyle(id: number) { const heights: Record = { 1: 100, 3: 150, 5: 200, 8: 100, 12: 150, }; return { height: heights[id], }; }, }, }; export const DynamicHeights: Story = { name: 'Dynamic heights', args: { debug: false, optimistic: false, getItemStyle(_: number, index: number) { const heights: Record = { 1: 100, 3: 150, 5: 200, 8: 100, 12: 150, }; return { height: heights[index], }; }, }, }; export const Clone: Story = { name: 'Clone feedback', args: { debug: false, plugins: [Feedback.configure({feedback: 'clone'})], }, }; export const RestrictAxis: Story = { name: 'Restrict axis', args: { debug: false, modifiers: [RestrictToVerticalAxis], }, }; export const DisabledItems: Story = { name: 'Disabled items', args: { debug: false, disabled: [2, 3, 8, 12], }, }; export const CustomTransition: Story = { name: 'Custom transition', args: { debug: false, transition: { duration: 300, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)', }, }, }; export const DisableTransition: Story = { name: 'Disable transition', args: { debug: false, transition: { duration: 0, }, }, }; export const Debug: Story = { name: 'Debug', args: { debug: true, }, }; type AutoScrollStory = StoryObj; export const AutoScrollDisabled: AutoScrollStory = { name: 'Auto-scroll disabled', render: (args: React.ComponentProps) => , args: { itemCount: 100, threshold: 0, }, }; export const AutoScrollCustomSpeed: AutoScrollStory = { name: 'Auto-scroll custom speed', render: (args: React.ComponentProps) => , args: { itemCount: 100, acceleration: 50, }, }; ================================================ FILE: apps/stories/stories/react/Sortable/Virtualized/ReactTinyVirtualListExample.tsx ================================================ import React, {useRef, useState} from 'react'; import type {UniqueIdentifier} from '@dnd-kit/abstract'; import {DragDropProvider} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {Debug} from '@dnd-kit/dom/plugins/debug'; import {move} from '@dnd-kit/helpers'; import VirtualList from 'react-tiny-virtual-list'; import {Item, Handle} from '../../components'; import {createRange} from '@dnd-kit/stories-shared/utilities'; interface Props { debug?: boolean; } export function ReactTinyVirtualListExample({debug}: Props) { const [items, setItems] = useState(createRange(1000)); const snapshot = useRef(structuredClone(items)); return ( [Debug, ...defaults] : undefined} onDragStart={() => { snapshot.current = structuredClone(items); }} onDragOver={(event) => { setItems((items) => move(items, event)); }} onDragEnd={(event) => { if (event.canceled) { setItems(snapshot.current); } }} > { const id = items[index]; return ; }} /> ); } interface SortableProps { id: UniqueIdentifier; index: number; style: React.CSSProperties; } function Sortable({id, index, style}: SortableProps) { const {isDragging, ref, handleRef} = useSortable({ id, index, }); return (
    } data-index={index} shadow={isDragging} > {id}
    ); } ================================================ FILE: apps/stories/stories/react/Sortable/Virtualized/ReactVirtualExample.tsx ================================================ import React, {forwardRef, useLayoutEffect, useRef, useState} from 'react'; import type {PropsWithChildren} from 'react'; import type {UniqueIdentifier} from '@dnd-kit/abstract'; import {DragDropProvider} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {Feedback} from '@dnd-kit/dom'; import {Debug} from '@dnd-kit/dom/plugins/debug'; import {move} from '@dnd-kit/helpers'; import {useWindowVirtualizer} from '@tanstack/react-virtual'; import {Item, Handle} from '../../components'; import {createRange} from '@dnd-kit/stories-shared/utilities'; interface Props { debug?: boolean; } export function ReactVirtualExample({debug}: Props) { const [items, setItems] = useState(createRange(1000)); const snapshot = useRef(structuredClone(items)); const parentRef = React.useRef(null); const parentOffsetRef = React.useRef(0); const virtualizer = useWindowVirtualizer({ count: items.length, estimateSize: () => 72, scrollMargin: parentOffsetRef.current, getItemKey: (index) => items[index], }); const virtualItems = virtualizer.getVirtualItems(); useLayoutEffect(() => { parentOffsetRef.current = parentRef.current?.offsetTop ?? 0; }, []); return ( [Debug, ...defaults] : undefined} onDragStart={() => { snapshot.current = structuredClone(items); }} onDragOver={(event) => { setItems((items) => move(items, event)); }} onDragEnd={(event) => { if (event.canceled) { setItems(snapshot.current); } }} >
    {virtualItems.map(({key, index}) => { return ( ); })}
    ); } interface SortableProps { id: UniqueIdentifier; index: number; } const Sortable = forwardRef>( function Sortable({id, index}, ref) { const [element, setElement] = useState(null); const handleRef = useRef(null); const {isDragging} = useSortable({ id, index, element, plugins: [Feedback.configure({feedback: 'clone'})], handle: handleRef, }); return ( } data-index={index} shadow={isDragging} > {id} ); } ); ================================================ FILE: apps/stories/stories/react/Sortable/Virtualized/ReactWindowExample.tsx ================================================ import React, {useRef, useEffect, useState} from 'react'; import type {UniqueIdentifier} from '@dnd-kit/abstract'; import {DragDropProvider} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {Debug} from '@dnd-kit/dom/plugins/debug'; import {move} from '@dnd-kit/helpers'; import {FixedSizeList as List} from 'react-window'; import {Item, Handle} from '../../components'; import {createRange} from '@dnd-kit/stories-shared/utilities'; interface Props { debug?: boolean; } export function ReactWindowExample({debug}: Props) { const [items, setItems] = useState(createRange(1000)); const snapshot = useRef(structuredClone(items)); return ( [Debug, ...defaults] : undefined} onDragStart={() => { snapshot.current = structuredClone(items); }} onDragOver={(event) => { setItems((items) => move(items, event)); }} onDragEnd={(event) => { if (event.canceled) { setItems(snapshot.current); } }} > items[index]} style={{margin: '0 auto'}} > {Row} ); } function Row({ data, index, style, }: { data: UniqueIdentifier[]; index: number; style: React.CSSProperties; }) { return ; } interface SortableProps { id: UniqueIdentifier; index: number; style: React.CSSProperties; } function Sortable({id, index, style}: SortableProps) { const {isDragging, ref, handleRef} = useSortable({ id, index, }); return (
    } data-index={index} shadow={isDragging} > {id}
    ); } ================================================ FILE: apps/stories/stories/react/Sortable/Virtualized/Virtualized.stories.tsx ================================================ import type {Meta, StoryObj} from '@storybook/react-vite'; import {ReactWindowExample} from './ReactWindowExample'; import {ReactVirtualExample} from './ReactVirtualExample'; import {ReactTinyVirtualListExample} from './ReactTinyVirtualListExample'; const meta: Meta = { title: 'React/Sortable/Virtualized', }; export default meta; type Story = StoryObj; export const ReactWindow: Story = { name: 'react-window', render: ReactWindowExample, }; export const ReactTinyVirtualList: Story = { name: 'react-tiny-virtual-list', render: ReactTinyVirtualListExample, }; export const ReactVirtual: Story = { name: 'react-virtual', render: ReactVirtualExample, }; ================================================ FILE: apps/stories/stories/react/Sortable/docs/SortableDocs.mdx ================================================ import {Code, Preview} from '../../../components'; import {Example as Hero} from '../Sortable.stories'; import BasicSetupSource from '../SortableExample?raw'; import {Example} from './examples/Example'; import {Example as UncontrolledExample} from './examples/UncontrolledExample'; import UncontrolledExampleSource from './examples/UncontrolledExample?raw'; import ControlledExampleSource from './examples/ControlledExample?raw'; import image from './assets/useSortable.png'; # Sortable Reorder elements in a list or across multiple lists. ## Installation Before getting started, make sure to install the `@dnd-kit/react` package: ```bash npm install @dnd-kit/react ``` ## Getting started The fastest way to get started is to the `useSortable` hook and provide it an `id` and an `index`: This will create an uncontrolled sortable list that will automatically update the order of the items in the DOM when they are dragged and dropped. To create a controlled sortable list and update the order of the items in React, we can listen to the `onDragEnd` event emitted by the `` component: To create a vertical list, we can update the of the `flex-direction` style property to `column`, without any other configuration changes. To create a grid list, we can update the of the `display` style property to `grid`, and set the `grid-template-columns` property to the desired number of columns. ## Architecture The sortable plugin composes the `Draggable` and `Droppable` items to provide a simple API for building sortable interfaces. The `useSortable` hook is an abstraction that composes the `useDroppable` and `useDraggable` hooks: As a result, the `useSortable` hook accepts all of the props that the `useDraggable` and `useDroppable` hooks accept. ## Modifiers Because the `useSortable` hook invokes the `useDraggable` hook under the hood, we can pass local `modifiers` to the `useSortable` hook just as we would with the `useDraggable` hook: ```jsx const sortable = useSortable({ id, modifiers: [RestrictToVerticalAxis], }); ``` ================================================ FILE: apps/stories/stories/react/Sortable/docs/examples/ControlledExample.jsx ================================================ import React, {useState} from 'react'; import {DragDropProvider} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {move} from '@dnd-kit/helpers'; const styles = {display: 'inline-flex', flexDirection: 'row', gap: 20}; export function ControlledExample() { const [items, setItems] = useState([0, 1, 2, 3]); return ( { setItems((items) => move(items, event)); }} >
    {items.map((id, index) => ( ))}
    ); } function Sortable({id, index}) { const {ref} = useSortable({id, index}); return ; } ================================================ FILE: apps/stories/stories/react/Sortable/docs/examples/Example.jsx ================================================ import React, {useState} from 'react'; import {DragDropProvider} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {move} from '@dnd-kit/helpers'; const styles = {display: 'inline-flex', flexDirection: 'row', gap: 20}; export function Example({style = styles}) { const [items, setItems] = useState([0, 1, 2, 3]); return ( { setItems((items) => move(items, event)); }} >
    {items.map((id, index) => ( ))}
    ); } function Sortable({id, index}) { const {ref} = useSortable({id, index}); return ; } ================================================ FILE: apps/stories/stories/react/Sortable/docs/examples/UncontrolledExample.jsx ================================================ import React, {useState} from 'react'; import {DragDropProvider} from '@dnd-kit/react'; import {useSortable} from '@dnd-kit/react/sortable'; import {move} from '@dnd-kit/helpers'; const styles = {display: 'inline-flex', flexDirection: 'row', gap: 20}; export function Example() { const items = [0, 1, 2, 3]; return (
    {items.map((id, index) => ( ))}
    ); } function Sortable({id, index}) { const {ref} = useSortable({id, index}); return ; } ================================================ FILE: apps/stories/stories/react/components/Actions/Action.tsx ================================================ import React, {forwardRef, CSSProperties, Ref} from 'react'; import {classNames} from '@dnd-kit/stories-shared/utilities'; import styles from './Actions.module.css'; export interface Props extends React.HTMLAttributes { variant?: 'light' | 'dark' | 'destructive'; cursor?: CSSProperties['cursor']; } export const Action = forwardRef( ({className, cursor, style, variant = 'light', ...props}, ref) => { return ( ); } ); ================================================ FILE: apps/stories/stories/react/components/Container/Container.tsx ================================================ import React, {forwardRef} from 'react'; export interface Props { children: React.ReactNode; actions?: React.ReactNode; columns?: number; label?: string; scrollable?: boolean; shadow?: boolean; style?: React.CSSProperties; transitionId?: string; } export const Container = forwardRef( ( { actions, children, columns = 1, label, style, scrollable, shadow, transitionId, ...props }: Props, ref ) => { return React.createElement( 'container-component', { ...props, ref, style: { ...style, viewTransitionName: transitionId, '--columns': columns, } as React.CSSProperties, 'data-shadow': shadow ? 'true' : undefined, 'data-scrollable': scrollable ? 'true' : undefined, }, <> {label ? (
    {label} {actions}
    ) : null}
      {children}
    ); } ); ================================================ FILE: apps/stories/stories/react/components/Dropzone/Dropzone.tsx ================================================ import React, {forwardRef} from 'react'; interface Props { children: React.ReactNode; highlight?: boolean; } export const Dropzone = forwardRef(function Dropzone( {children, highlight}, ref ) { return (
    {children}
    ); }); ================================================ FILE: apps/stories/stories/react/components/Grid/Grid.module.css ================================================ .Grid { position: fixed; inset: 0; background-image: repeating-linear-gradient( 0deg, transparent, transparent calc(var(--grid-size) - 1px), #ddd calc(var(--grid-size) - 1px), #ddd var(--grid-size) ), repeating-linear-gradient( -90deg, transparent, transparent calc(var(--grid-size) - 1px), #ddd calc(var(--grid-size) - 1px), #ddd var(--grid-size) ); background-size: var(--grid-size) var(--grid-size); } ================================================ FILE: apps/stories/stories/react/components/Grid/Grid.tsx ================================================ import React, {type PropsWithChildren} from 'react'; import styles from './Grid.module.css'; export interface Props extends PropsWithChildren { size: number; } export function Grid({children, size}: Props) { return (
    {children}
    ); } ================================================ FILE: apps/stories/stories/react/components/Handle/Handle.tsx ================================================ import React, {forwardRef} from 'react'; export interface HandleProps extends React.HTMLAttributes { variant?: string; } export const Handle = forwardRef( ({variant, ...props}, ref) => { return
    ); } export default function App() { return ( ); } ================================================ FILE: apps/stories-solid/stories/Draggable/Draggable.stories.tsx ================================================ import type {Meta, StoryObj} from 'storybook-solidjs'; import App from './DraggableApp'; import draggableSource from './DraggableApp.tsx?raw'; import {baseStyles, draggableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Draggable/Basic setup', component: App, }; export default meta; type Story = StoryObj; export const Example: Story = { parameters: { codesandbox: { files: { 'src/App.tsx': draggableSource, 'src/styles.css': [baseStyles, draggableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-solid/stories/Draggable/DraggableApp.tsx ================================================ import {DragDropProvider, useDraggable} from '@dnd-kit/solid'; function Draggable() { const {ref} = useDraggable({id: 'draggable'}); return ; } export default function App() { return ( ); } ================================================ FILE: apps/stories-solid/stories/Droppable/Droppable.stories.tsx ================================================ import type {Meta, StoryObj} from 'storybook-solidjs'; import App from './DroppableApp'; import droppableSource from './DroppableApp.tsx?raw'; import {baseStyles, draggableStyles, droppableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Droppable/Basic setup', component: App, }; export default meta; type Story = StoryObj; export const Example: Story = { parameters: { codesandbox: { files: { 'src/App.tsx': droppableSource, 'src/styles.css': [baseStyles, draggableStyles, droppableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-solid/stories/Droppable/DroppableApp.tsx ================================================ import {createSignal} from 'solid-js'; import {DragDropProvider, useDraggable, useDroppable} from '@dnd-kit/solid'; function Draggable() { const {ref} = useDraggable({id: 'draggable'}); return ; } function Droppable(props: {children?: any}) { const {isDropTarget, ref} = useDroppable({id: 'droppable'}); return (
    {props.children}
    ); } export default function App() { const [parent, setParent] = createSignal(); return ( { if (event.canceled) return; setParent(event.operation.target?.id as string); }} >
    {parent() == null ? : null} {parent() === 'droppable' ? : null}
    ); } ================================================ FILE: apps/stories-solid/stories/Droppable/MultipleDroppable/MultipleDroppable.stories.tsx ================================================ import type {Meta, StoryObj} from 'storybook-solidjs'; import App from './MultipleDroppableApp'; import multipleDroppableSource from './MultipleDroppableApp.tsx?raw'; import {baseStyles, draggableStyles, droppableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Droppable/Multiple drop targets', component: App, }; export default meta; type Story = StoryObj; export const Example: Story = { parameters: { codesandbox: { files: { 'src/App.tsx': multipleDroppableSource, 'src/styles.css': [baseStyles, draggableStyles, droppableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-solid/stories/Droppable/MultipleDroppable/MultipleDroppableApp.tsx ================================================ import {createSignal, For} from 'solid-js'; import {DragDropProvider, useDraggable, useDroppable} from '@dnd-kit/solid'; function Draggable() { const {ref} = useDraggable({id: 'draggable'}); return ; } function Droppable(props: {id: string; children?: any}) { const {isDropTarget, ref} = useDroppable({id: props.id}); return (
    {props.children}
    ); } export default function App() { const [parent, setParent] = createSignal(); const droppables = ['A', 'B', 'C']; return ( { if (event.canceled) return; setParent(event.operation.target?.id as string); }} >
    {parent() == null ? : null}
    {(id) => ( {parent() === id ? : null} )}
    ); } ================================================ FILE: apps/stories-solid/stories/Sortable/Grid/Grid.stories.tsx ================================================ import type {Meta, StoryObj} from 'storybook-solidjs'; import App from './GridSortableApp'; import gridSortableSource from './GridSortableApp.tsx?raw'; import {baseStyles, sortableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Sortable/Grid', component: App, }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', parameters: { codesandbox: { files: { 'src/App.tsx': gridSortableSource, 'src/styles.css': [baseStyles, sortableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-solid/stories/Sortable/Grid/GridSortableApp.tsx ================================================ import {createSignal, For} from 'solid-js'; import {DragDropProvider} from '@dnd-kit/solid'; import {useSortable} from '@dnd-kit/solid/sortable'; import {move} from '@dnd-kit/helpers'; function Sortable(props: {id: number; index: number}) { const {isDragging, ref} = useSortable({ get id() { return props.id; }, get index() { return props.index; }, }); return (
    {props.id}
    ); } export default function App() { const [items, setItems] = createSignal(createRange(20)); return ( { setItems((items) => move(items, event)); }} >
    {(id, index) => }
    ); } function createRange(length: number) { return Array.from({length}, (_, i) => i + 1); } ================================================ FILE: apps/stories-solid/stories/Sortable/Horizontal/Horizontal.stories.tsx ================================================ import type {Meta, StoryObj} from 'storybook-solidjs'; import App from './HorizontalSortableApp'; import horizontalSortableSource from './HorizontalSortableApp.tsx?raw'; import {baseStyles, sortableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Sortable/Horizontal list', component: App, }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', parameters: { codesandbox: { files: { 'src/App.tsx': horizontalSortableSource, 'src/styles.css': [baseStyles, sortableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-solid/stories/Sortable/Horizontal/HorizontalSortableApp.tsx ================================================ import {createSignal, For} from 'solid-js'; import {DragDropProvider} from '@dnd-kit/solid'; import {useSortable} from '@dnd-kit/solid/sortable'; import {move} from '@dnd-kit/helpers'; function Sortable(props: {id: number; index: number}) { const {isDragging, ref} = useSortable({ get id() { return props.id; }, get index() { return props.index; }, }); return (
    {props.id}
    ); } export default function App() { const [items, setItems] = createSignal(createRange(10)); return ( { setItems((items) => move(items, event)); }} >
    {(id, index) => }
    ); } function createRange(length: number) { return Array.from({length}, (_, i) => i + 1); } ================================================ FILE: apps/stories-solid/stories/Sortable/MultipleLists/MultipleLists.stories.tsx ================================================ import type {Meta, StoryObj} from 'storybook-solidjs'; import MultipleListsApp from './MultipleListsApp.tsx'; import multipleListsSource from './MultipleListsApp.tsx?raw'; import {baseStyles, sortableStyles, multipleListsStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const styles = [baseStyles, sortableStyles, multipleListsStyles].join('\n\n'); const meta: Meta = { title: 'Sortable/Multiple lists', component: MultipleListsApp, }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', render: () => ( <> ), parameters: { codesandbox: { files: { 'src/App.tsx': multipleListsSource, 'src/styles.css': styles, }, }, }, }; ================================================ FILE: apps/stories-solid/stories/Sortable/MultipleLists/MultipleListsApp.tsx ================================================ import {createSignal, For} from 'solid-js'; import {CollisionPriority} from '@dnd-kit/abstract'; import {DragDropProvider} from '@dnd-kit/solid'; import {useSortable} from '@dnd-kit/solid/sortable'; import {defaultPreset, PointerSensor, KeyboardSensor} from '@dnd-kit/dom'; import {move} from '@dnd-kit/helpers'; function createRange(length: number) { return Array.from({length}, (_, i) => i + 1); } const ITEM_COUNT = 6; const COLORS: Record = { A: '#7193f1', B: '#FF851B', C: '#2ECC40', D: '#ff3680', }; const sensors = [ PointerSensor.configure({ activatorElements(source) { return [source.element, source.handle]; }, }), KeyboardSensor, ]; function SortableItem(props: {id: string; column: string; index: number}) { const {isDragging, ref, handleRef} = useSortable({ get id() { return props.id; }, get index() { return props.index; }, get group() { return props.column; }, get data() { return {group: props.column}; }, accept: 'item', type: 'item', feedback: 'clone', }); return (
  • {props.id}
  • ); } function SortableColumn(props: {id: string; index: number; rows: string[]}) { const {isDragging, ref, handleRef} = useSortable({ get id() { return props.id; }, get index() { return props.index; }, accept: ['column', 'item'], collisionPriority: CollisionPriority.Low, type: 'column', }); return (

    {props.id}

      {(itemId, itemIndex) => ( )}
    ); } export default function App() { const [items, setItems] = createSignal>({ A: createRange(ITEM_COUNT).map((id) => `A${id}`), B: createRange(ITEM_COUNT).map((id) => `B${id}`), C: createRange(ITEM_COUNT).map((id) => `C${id}`), D: [], }); const columns = Object.keys(items()); let snapshot = structuredClone(items()); return ( { snapshot = structuredClone(items()); }} onDragOver={(event) => { const {source} = event.operation; if (source && source.type === 'column') return; event.preventDefault(); setItems((items) => move(items, event)); }} onDragEnd={(event) => { if (event.canceled) { setItems(snapshot); } }} >
    {(column, columnIndex) => ( )}
    ); } ================================================ FILE: apps/stories-solid/stories/Sortable/SortableApp.tsx ================================================ import {createSignal, For} from 'solid-js'; import {DragDropProvider} from '@dnd-kit/solid'; import {useSortable} from '@dnd-kit/solid/sortable'; import {move} from '@dnd-kit/helpers'; function Sortable(props: {id: number; index: number}) { const {isDragging, ref} = useSortable({ get id() { return props.id; }, get index() { return props.index; }, }); return (
  • {props.id}
  • ); } export default function App() { const [items, setItems] = createSignal(createRange(100)); return ( { setItems((items) => move(items, event)); }} >
      {(id, index) => }
    ); } function createRange(length: number) { return Array.from({length}, (_, i) => i + 1); } ================================================ FILE: apps/stories-solid/stories/Sortable/SortableDragHandleApp.tsx ================================================ import {createSignal, For} from 'solid-js'; import {DragDropProvider} from '@dnd-kit/solid'; import {useSortable} from '@dnd-kit/solid/sortable'; import {move} from '@dnd-kit/helpers'; function Sortable(props: {id: number; index: number}) { const {isDragging, ref, handleRef} = useSortable({ get id() { return props.id; }, get index() { return props.index; }, }); return (
  • {props.id}
  • ); } export default function App() { const [items, setItems] = createSignal(createRange(100)); return ( { setItems((items) => move(items, event)); }} >
      {(id, index) => }
    ); } function createRange(length: number) { return Array.from({length}, (_, i) => i + 1); } ================================================ FILE: apps/stories-solid/stories/Sortable/Vertical/Vertical.stories.tsx ================================================ import type {Meta, StoryObj} from 'storybook-solidjs'; import App from '../SortableApp'; import sortableSource from '../SortableApp.tsx?raw'; import DragHandleApp from '../SortableDragHandleApp'; import sortableDragHandleSource from '../SortableDragHandleApp.tsx?raw'; import { baseStyles, handleStyles, sortableStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Sortable/Vertical list', component: App, }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', parameters: { codesandbox: { files: { 'src/App.tsx': sortableSource, 'src/styles.css': [baseStyles, sortableStyles].join('\n\n'), }, }, }, }; export const WithDragHandle: Story = { name: 'Drag handle', render: () => , decorators: [ (Story) => ( <> ), ], parameters: { codesandbox: { files: { 'src/App.tsx': sortableDragHandleSource, 'src/styles.css': [baseStyles, sortableStyles, handleStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-solid/tests/draggable.spec.ts ================================================ import {draggableTests} from '../../stories-shared/tests/draggable.tests.ts'; draggableTests({ example: 'draggable-basic-setup--example', dragHandle: 'draggable-drag-handles--example', }); ================================================ FILE: apps/stories-solid/tests/droppable.spec.ts ================================================ import {droppableTests} from '../../stories-shared/tests/droppable.tests.ts'; droppableTests({ example: 'droppable-basic-setup--example', multipleDropTargets: 'droppable-multiple-drop-targets--example', }); ================================================ FILE: apps/stories-solid/tests/sortable-vertical.spec.ts ================================================ import {sortableVerticalTests} from '../../stories-shared/tests/sortable-vertical.tests.ts'; sortableVerticalTests({ basicSetup: 'sortable-vertical-list--basic-setup', withDragHandle: 'sortable-vertical-list--with-drag-handle', }); ================================================ FILE: apps/stories-solid/tsconfig.json ================================================ { "extends": "../../config/typescript/solid.json", "include": ["."], "exclude": ["dist", "build", "node_modules"] } ================================================ FILE: apps/stories-svelte/.storybook/main.ts ================================================ import {readFileSync} from 'fs'; import {join} from 'path'; const sharedPreviewHead = readFileSync( join(__dirname, '..', '..', 'stories-shared', 'preview-head.html'), 'utf-8' ); export default { previewHead: (head: string) => `${sharedPreviewHead}\n${head}`, stories: ['../stories/**/*.stories.ts'], addons: [ '@storybook/addon-links', '@vueless/storybook-dark-mode', '@dnd-kit/storybook-addon-codesandbox', ], framework: '@storybook/svelte-vite', async viteFinal(config: any) { config.define = { ...config.define, 'process.env': {}, }; config.optimizeDeps = { ...config.optimizeDeps, exclude: [...(config.optimizeDeps?.exclude || []), '@dnd-kit/*'], }; return config; }, }; ================================================ FILE: apps/stories-svelte/.storybook/manager.ts ================================================ import {addons} from 'storybook/manager-api'; import {theme} from './theme'; addons.setConfig({ theme, showPanel: false, }); ================================================ FILE: apps/stories-svelte/.storybook/preview.ts ================================================ import '@dnd-kit/stories-shared/register'; import {withCodeSandbox} from '@dnd-kit/storybook-addon-codesandbox/decorator-dom'; const preview = { decorators: [withCodeSandbox], parameters: { codesandbox: { provider: 'stackblitz', files: { 'index.html': [ '', '', '', ' ', '', '', '
    ', ' ', '', '', ].join('\n'), 'src/main.js': [ "import './styles.css';", "import {mount} from 'svelte';", "import App from './App.svelte';", '', "mount(App, {target: document.getElementById('app')});", ].join('\n'), 'vite.config.js': [ "import {svelte} from '@sveltejs/vite-plugin-svelte';", "import {defineConfig} from 'vite';", '', 'export default defineConfig({', ' plugins: [svelte()],', '});', ].join('\n'), 'package.json': JSON.stringify( { name: 'dnd-kit-sandbox', private: true, type: 'module', scripts: { dev: 'vite dev', start: 'vite dev', }, dependencies: { '@dnd-kit/abstract': 'beta', '@dnd-kit/dom': 'beta', '@dnd-kit/svelte': 'beta', '@dnd-kit/helpers': 'beta', '@dnd-kit/collision': 'beta', svelte: '^5.29.0', }, devDependencies: { vite: '^6.0.0', '@sveltejs/vite-plugin-svelte': '^5.0.0', }, }, null, 2 ), }, mainFile: 'src/App.svelte', }, darkMode: { stylePreview: true, }, options: { storySort: { order: [ 'Draggable', ['Basic setup', 'Drag handles', 'Drag overlay'], 'Droppable', 'Sortable', ['Vertical list', 'Horizontal list', 'Grid', 'Multiple lists'], ], }, }, }, }; export default preview; ================================================ FILE: apps/stories-svelte/.storybook/theme.ts ================================================ import {create} from 'storybook/theming/create'; import {default as brandImage} from './assets/dnd-kit-banner.svg'; export const theme = create({ base: 'light', brandImage, appBg: '#F9F9F9', }); ================================================ FILE: apps/stories-svelte/package.json ================================================ { "name": "@dnd-kit/stories-svelte", "version": "0.0.0", "type": "module", "private": true, "scripts": { "dev": "storybook dev -p 6010", "build": "storybook build", "clean": "rm -rf .turbo && rm -rf node_modules", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui" }, "dependencies": { "@dnd-kit/abstract": "*", "@dnd-kit/dom": "*", "@dnd-kit/svelte": "*", "@dnd-kit/helpers": "*", "@dnd-kit/stories-shared": "*", "@dnd-kit/storybook-addon-codesandbox": "*", "svelte": "^5.29.0" }, "devDependencies": { "@playwright/test": "^1.58.2", "@storybook/addon-links": "^9.0.15", "@storybook/svelte-vite": "9.1.17", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@vueless/storybook-dark-mode": "^9.0.6", "storybook": "9.1.17", "typescript": "^5.7.3", "vite": "^6.0.0" } } ================================================ FILE: apps/stories-svelte/playwright.config.ts ================================================ import {defineConfig} from '@playwright/test'; const CI = !!process.env.CI; export default defineConfig({ testDir: './tests', timeout: 15_000, expect: { timeout: 5_000, }, fullyParallel: true, retries: CI ? 2 : 1, reporter: CI ? 'html' : 'list', use: { baseURL: 'http://localhost:6010', actionTimeout: 5_000, }, projects: [ { name: 'chromium', use: {browserName: 'chromium'}, }, ], webServer: { command: 'npx http-server storybook-static --port 6010 --silent', port: 6010, reuseExistingServer: !CI, timeout: 120_000, }, }); ================================================ FILE: apps/stories-svelte/raw.d.ts ================================================ declare module '*.svelte?raw' { const content: string; export default content; } ================================================ FILE: apps/stories-svelte/stories/Draggable/DragHandles/DragHandles.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/svelte-vite'; import DragHandlesApp from './DragHandlesApp.svelte'; import dragHandlesSource from './DragHandlesApp.svelte?raw'; import draggableWithHandleSource from './DraggableWithHandle.svelte?raw'; import { baseStyles, draggableStyles, handleStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Draggable/Drag handles', component: DragHandlesApp, }; export default meta; type Story = StoryObj; export const Example: Story = { parameters: { codesandbox: { files: { 'src/App.svelte': dragHandlesSource, 'src/DraggableWithHandle.svelte': draggableWithHandleSource, 'src/styles.css': [baseStyles, draggableStyles, handleStyles].join( '\n\n' ), }, }, }, }; ================================================ FILE: apps/stories-svelte/stories/Draggable/DragHandles/DragHandlesApp.svelte ================================================ ================================================ FILE: apps/stories-svelte/stories/Draggable/DragHandles/DraggableWithHandle.svelte ================================================
    draggable
    ================================================ FILE: apps/stories-svelte/stories/Draggable/DragOverlay/DragOverlay.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/svelte-vite'; import DragOverlayApp from './DragOverlayApp.svelte'; import dragOverlaySource from './DragOverlayApp.svelte?raw'; import draggableItemSource from './DraggableItem.svelte?raw'; import { baseStyles, draggableStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Draggable/Drag overlay', component: DragOverlayApp, }; export default meta; type Story = StoryObj; export const Example: Story = { parameters: { codesandbox: { files: { 'src/App.svelte': dragOverlaySource, 'src/DraggableItem.svelte': draggableItemSource, 'src/styles.css': [baseStyles, draggableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-svelte/stories/Draggable/DragOverlay/DragOverlayApp.svelte ================================================
    {#snippet children(source)} {/snippet}
    ================================================ FILE: apps/stories-svelte/stories/Draggable/DragOverlay/DraggableItem.svelte ================================================ ================================================ FILE: apps/stories-svelte/stories/Draggable/Draggable.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/svelte-vite'; import DraggableApp from './DraggableApp.svelte'; import draggableSource from './DraggableApp.svelte?raw'; import draggableComponentSource from './Draggable.svelte?raw'; import { baseStyles, draggableStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Draggable/Basic setup', component: DraggableApp, }; export default meta; type Story = StoryObj; export const Example: Story = { parameters: { codesandbox: { files: { 'src/App.svelte': draggableSource, 'src/Draggable.svelte': draggableComponentSource, 'src/styles.css': [baseStyles, draggableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-svelte/stories/Draggable/Draggable.svelte ================================================ ================================================ FILE: apps/stories-svelte/stories/Draggable/DraggableApp.svelte ================================================ ================================================ FILE: apps/stories-svelte/stories/Droppable/DraggableItem.svelte ================================================ ================================================ FILE: apps/stories-svelte/stories/Droppable/Droppable.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/svelte-vite'; import DroppableApp from './DroppableApp.svelte'; import droppableSource from './DroppableApp.svelte?raw'; import draggableItemSource from './DraggableItem.svelte?raw'; import droppableZoneSource from './DroppableZone.svelte?raw'; import { baseStyles, draggableStyles, droppableStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Droppable/Basic setup', component: DroppableApp, }; export default meta; type Story = StoryObj; export const Example: Story = { parameters: { codesandbox: { files: { 'src/App.svelte': droppableSource, 'src/DraggableItem.svelte': draggableItemSource, 'src/DroppableZone.svelte': droppableZoneSource, 'src/styles.css': [baseStyles, draggableStyles, droppableStyles].join( '\n\n' ), }, }, }, }; ================================================ FILE: apps/stories-svelte/stories/Droppable/DroppableApp.svelte ================================================
    {#if parent == null} {/if} {#if parent === 'droppable'} {/if}
    ================================================ FILE: apps/stories-svelte/stories/Droppable/DroppableZone.svelte ================================================
    {@render children?.()}
    ================================================ FILE: apps/stories-svelte/stories/Droppable/MultipleDroppable/MultipleDroppable.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/svelte-vite'; import MultipleDroppableApp from './MultipleDroppableApp.svelte'; import multipleDroppableSource from './MultipleDroppableApp.svelte?raw'; import draggableItemSource from '../DraggableItem.svelte?raw'; import droppableZoneSource from '../DroppableZone.svelte?raw'; import { baseStyles, draggableStyles, droppableStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Droppable/Multiple drop targets', component: MultipleDroppableApp, }; export default meta; type Story = StoryObj; export const Example: Story = { parameters: { codesandbox: { files: { 'src/App.svelte': multipleDroppableSource.replace( /from '\.\.\/(\w+\.svelte)'/g, "from './$1'" ), 'src/DraggableItem.svelte': draggableItemSource, 'src/DroppableZone.svelte': droppableZoneSource, 'src/styles.css': [baseStyles, draggableStyles, droppableStyles].join( '\n\n' ), }, }, }, }; ================================================ FILE: apps/stories-svelte/stories/Droppable/MultipleDroppable/MultipleDroppableApp.svelte ================================================
    {#if parent == null} {/if}
    {#each droppables as id (id)} {#if parent === id} {/if} {/each}
    ================================================ FILE: apps/stories-svelte/stories/Sortable/Grid/Grid.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/svelte-vite'; import GridSortableApp from './GridSortableApp.svelte'; import gridSortableSource from './GridSortableApp.svelte?raw'; import gridSortableItemSource from './GridSortableItem.svelte?raw'; import { baseStyles, sortableStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Sortable/Grid', }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', render: () => ({Component: GridSortableApp}), parameters: { codesandbox: { files: { 'src/App.svelte': gridSortableSource, 'src/GridSortableItem.svelte': gridSortableItemSource, 'src/styles.css': [baseStyles, sortableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-svelte/stories/Sortable/Grid/GridSortableApp.svelte ================================================
    {#each items as id, index (id)} {/each}
    ================================================ FILE: apps/stories-svelte/stories/Sortable/Grid/GridSortableItem.svelte ================================================
    {id}
    ================================================ FILE: apps/stories-svelte/stories/Sortable/Horizontal/Horizontal.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/svelte-vite'; import HorizontalSortableApp from './HorizontalSortableApp.svelte'; import horizontalSortableSource from './HorizontalSortableApp.svelte?raw'; import horizontalSortableItemSource from './HorizontalSortableItem.svelte?raw'; import { baseStyles, sortableStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Sortable/Horizontal list', }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', render: () => ({Component: HorizontalSortableApp}), parameters: { codesandbox: { files: { 'src/App.svelte': horizontalSortableSource, 'src/HorizontalSortableItem.svelte': horizontalSortableItemSource, 'src/styles.css': [baseStyles, sortableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-svelte/stories/Sortable/Horizontal/HorizontalSortableApp.svelte ================================================
    {#each items as id, index (id)} {/each}
    ================================================ FILE: apps/stories-svelte/stories/Sortable/Horizontal/HorizontalSortableItem.svelte ================================================
    {id}
    ================================================ FILE: apps/stories-svelte/stories/Sortable/MultipleLists/MultipleLists.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/svelte-vite'; import MultipleListsApp from './MultipleListsApp.svelte'; import multipleListsSource from './MultipleListsApp.svelte?raw'; import sortableColumnSource from './SortableColumn.svelte?raw'; import sortableItemSource from './SortableItem.svelte?raw'; import { baseStyles, sortableStyles, multipleListsStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; const styles = [baseStyles, sortableStyles, multipleListsStyles].join('\n\n'); const meta: Meta = { title: 'Sortable/Multiple lists', component: MultipleListsApp, }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', parameters: { codesandbox: { files: { 'src/App.svelte': multipleListsSource, 'src/SortableColumn.svelte': sortableColumnSource, 'src/SortableItem.svelte': sortableItemSource, 'src/styles.css': styles, }, }, }, }; ================================================ FILE: apps/stories-svelte/stories/Sortable/MultipleLists/MultipleListsApp.svelte ================================================
    {#each columns as column, columnIndex (column)} {/each}
    ================================================ FILE: apps/stories-svelte/stories/Sortable/MultipleLists/SortableColumn.svelte ================================================

    {id}

      {#each rows as itemId, itemIndex (itemId)} {/each}
    ================================================ FILE: apps/stories-svelte/stories/Sortable/MultipleLists/SortableItem.svelte ================================================
  • {id}
  • ================================================ FILE: apps/stories-svelte/stories/Sortable/SortableApp.svelte ================================================
      {#each items as id, index (id)} {/each}
    ================================================ FILE: apps/stories-svelte/stories/Sortable/SortableDragHandleApp.svelte ================================================
      {#each items as id, index (id)} {/each}
    ================================================ FILE: apps/stories-svelte/stories/Sortable/SortableItem.svelte ================================================
  • {id}
  • ================================================ FILE: apps/stories-svelte/stories/Sortable/SortableItemWithHandle.svelte ================================================
  • {id}
  • ================================================ FILE: apps/stories-svelte/stories/Sortable/Vertical/Vertical.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/svelte-vite'; import SortableApp from '../SortableApp.svelte'; import sortableSource from '../SortableApp.svelte?raw'; import sortableItemSource from '../SortableItem.svelte?raw'; import SortableDragHandleApp from '../SortableDragHandleApp.svelte'; import sortableDragHandleSource from '../SortableDragHandleApp.svelte?raw'; import sortableItemWithHandleSource from '../SortableItemWithHandle.svelte?raw'; import { baseStyles, handleStyles, sortableStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Sortable/Vertical list', }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', render: () => ({Component: SortableApp}), parameters: { codesandbox: { files: { 'src/App.svelte': sortableSource, 'src/SortableItem.svelte': sortableItemSource, 'src/styles.css': [baseStyles, sortableStyles].join('\n\n'), }, }, }, }; export const WithDragHandle: Story = { name: 'Drag handle', render: () => ({Component: SortableDragHandleApp}), parameters: { codesandbox: { files: { 'src/App.svelte': sortableDragHandleSource, 'src/SortableItemWithHandle.svelte': sortableItemWithHandleSource, 'src/styles.css': [baseStyles, sortableStyles, handleStyles].join( '\n\n' ), }, }, }, }; ================================================ FILE: apps/stories-svelte/tests/draggable.spec.ts ================================================ import {draggableTests} from '../../stories-shared/tests/draggable.tests.ts'; draggableTests({ example: 'draggable-basic-setup--example', dragHandle: 'draggable-drag-handles--example', }); ================================================ FILE: apps/stories-svelte/tests/droppable.spec.ts ================================================ import {droppableTests} from '../../stories-shared/tests/droppable.tests.ts'; droppableTests({ example: 'droppable-basic-setup--example', multipleDropTargets: 'droppable-multiple-drop-targets--example', }); ================================================ FILE: apps/stories-svelte/tests/sortable-vertical.spec.ts ================================================ import {sortableVerticalTests} from '../../stories-shared/tests/sortable-vertical.tests.ts'; sortableVerticalTests({ basicSetup: 'sortable-vertical-list--basic-setup', withDragHandle: 'sortable-vertical-list--with-drag-handle', }); ================================================ FILE: apps/stories-svelte/vite.config.ts ================================================ import {defineConfig} from 'vite'; import {svelte} from '@sveltejs/vite-plugin-svelte'; export default defineConfig({ plugins: [ svelte(), ], }); ================================================ FILE: apps/stories-vanilla/.storybook/main.ts ================================================ import {readFileSync} from 'fs'; import {dirname, join} from 'path'; import {mergeConfig} from 'vite'; const sharedPreviewHead = readFileSync( join(__dirname, '..', '..', 'stories-shared', 'preview-head.html'), 'utf-8' ); export default { previewHead: (head: string) => `${sharedPreviewHead}\n${head}`, stories: ['../stories/**/*.stories.ts'], addons: [ getAbsolutePath('@storybook/addon-links'), getAbsolutePath('@vueless/storybook-dark-mode'), getAbsolutePath('@dnd-kit/storybook-addon-codesandbox'), ], framework: { name: getAbsolutePath('@storybook/html-vite'), options: {}, }, async viteFinal(config) { return mergeConfig(config, { define: { 'process.env': {}, }, optimizeDeps: { exclude: ['@dnd-kit/*'], }, }); }, }; function getAbsolutePath(value) { return dirname(require.resolve(join(value, 'package.json'))); } ================================================ FILE: apps/stories-vanilla/.storybook/manager-head.html ================================================ ================================================ FILE: apps/stories-vanilla/.storybook/manager.ts ================================================ import {addons} from 'storybook/manager-api'; import {theme} from './theme'; addons.setConfig({ theme, showPanel: false, }); ================================================ FILE: apps/stories-vanilla/.storybook/preview.ts ================================================ import '@dnd-kit/stories-shared/register'; import {withCodeSandbox} from '@dnd-kit/storybook-addon-codesandbox/decorator-dom'; const preview = { decorators: [withCodeSandbox], parameters: { codesandbox: { dependencies: { '@dnd-kit/abstract': 'beta', '@dnd-kit/dom': 'beta', '@dnd-kit/helpers': 'beta', '@dnd-kit/collision': 'beta', }, entry: [ "import './styles.css';", "import App from './App';", "", "App();", ].join('\n'), mainFile: 'src/App.ts', }, darkMode: { stylePreview: true, }, options: { storySort: { order: [ 'Draggable', [ 'Basic setup', 'Drag handle', ], 'Droppable', 'Sortable', [ 'Vertical list', 'Horizontal list', 'Grid', 'Multiple lists', ], ], }, }, }, }; export default preview; ================================================ FILE: apps/stories-vanilla/.storybook/theme.ts ================================================ import {create} from 'storybook/theming/create'; import {default as brandImage} from './assets/dnd-kit-banner.svg'; export const theme = create({ base: 'light', brandImage, appBg: '#F9F9F9', }); ================================================ FILE: apps/stories-vanilla/package.json ================================================ { "name": "@dnd-kit/stories-vanilla", "version": "0.0.0", "type": "module", "private": true, "scripts": { "dev": "storybook dev -p 6007", "build": "storybook build", "clean": "rm -rf .turbo && rm -rf node_modules" }, "dependencies": { "@dnd-kit/abstract": "*", "@dnd-kit/dom": "*", "@dnd-kit/helpers": "*", "@dnd-kit/stories-shared": "*", "@dnd-kit/storybook-addon-codesandbox": "*" }, "devDependencies": { "@storybook/addon-links": "^9.0.15", "@storybook/html-vite": "^9.0.15", "@vueless/storybook-dark-mode": "^9.0.6", "storybook": "^9.0.15", "typescript": "^5.7.3", "vite": "^6.0.0" } } ================================================ FILE: apps/stories-vanilla/raw.d.ts ================================================ declare module '*.ts?raw' { const content: string; export default content; } declare module '*.js?raw' { const content: string; export default content; } ================================================ FILE: apps/stories-vanilla/stories/Draggable/DragHandle/DragHandle.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/html-vite'; import App from './DragHandleApp.ts'; import dragHandleSource from './DragHandleApp.ts?raw'; import {baseStyles, draggableStyles, handleStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Draggable/Drag handle', }; export default meta; type Story = StoryObj; export const Example: Story = { render: () => App(), parameters: { codesandbox: { files: { 'src/App.ts': dragHandleSource, 'src/styles.css': [baseStyles, draggableStyles, handleStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-vanilla/stories/Draggable/DragHandle/DragHandleApp.ts ================================================ import {DragDropManager, Draggable} from '@dnd-kit/dom'; export default function App() { const root = document.createElement('div'); const button = document.createElement('div'); button.className = 'btn'; button.append('draggable'); const handle = document.createElement('button'); handle.className = 'handle'; button.appendChild(handle); root.appendChild(button); const manager = new DragDropManager(); new Draggable( { id: 'draggable', element: button, handle, }, manager ); return root; } ================================================ FILE: apps/stories-vanilla/stories/Draggable/Draggable.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/html-vite'; import App from './DraggableApp.ts'; import draggableSource from './DraggableApp.ts?raw'; import {baseStyles, draggableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Draggable/Basic setup', }; export default meta; type Story = StoryObj; export const Example: Story = { render: () => App(), parameters: { codesandbox: { files: { 'src/App.ts': draggableSource, 'src/styles.css': [baseStyles, draggableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-vanilla/stories/Draggable/DraggableApp.ts ================================================ import {DragDropManager, Draggable} from '@dnd-kit/dom'; export default function App() { const root = document.createElement('div'); const button = document.createElement('button'); button.className = 'btn'; button.textContent = 'draggable'; root.appendChild(button); const manager = new DragDropManager(); new Draggable({id: 'draggable', element: button}, manager); return root; } ================================================ FILE: apps/stories-vanilla/stories/Droppable/Droppable.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/html-vite'; import App from './DroppableApp.ts'; import droppableSource from './DroppableApp.ts?raw'; import { baseStyles, draggableStyles, droppableStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Droppable/Basic setup', }; export default meta; type Story = StoryObj; export const Example: Story = { render: () => App(), parameters: { codesandbox: { files: { 'src/App.ts': droppableSource, 'src/styles.css': [baseStyles, draggableStyles, droppableStyles].join( '\n\n' ), }, }, }, }; ================================================ FILE: apps/stories-vanilla/stories/Droppable/DroppableApp.ts ================================================ import {DragDropManager, Draggable, Droppable} from '@dnd-kit/dom'; export default function App() { const section = document.createElement('section'); section.className = 'drop-layout'; const button = document.createElement('button'); button.className = 'btn'; button.textContent = 'draggable'; const dropzone = document.createElement('div'); dropzone.className = 'droppable'; section.append(button, dropzone); const manager = new DragDropManager(); const draggable = new Draggable({id: 'draggable', element: button}, manager); const droppable = new Droppable( { id: 'droppable', element: dropzone, effects: () => [ () => { if (droppable.isDropTarget) { dropzone.classList.add('active'); return () => dropzone.classList.remove('active'); } }, ], }, manager ); manager.monitor.addEventListener('dragend', (event) => { if (event.canceled) return; const isInside = dropzone.contains(button); if (event.operation.target?.id === 'droppable') { if (!isInside) dropzone.appendChild(button); } else if (isInside) { section.prepend(button); } }); return section; } ================================================ FILE: apps/stories-vanilla/stories/Droppable/MultipleDroppable/MultipleDroppable.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/html-vite'; import App from './MultipleDroppableApp.ts'; import multipleDroppableSource from './MultipleDroppableApp.ts?raw'; import { baseStyles, draggableStyles, droppableStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Droppable/Multiple drop targets', }; export default meta; type Story = StoryObj; export const Example: Story = { render: () => App(), parameters: { codesandbox: { files: { 'src/App.ts': multipleDroppableSource, 'src/styles.css': [baseStyles, draggableStyles, droppableStyles].join( '\n\n' ), }, }, }, }; ================================================ FILE: apps/stories-vanilla/stories/Droppable/MultipleDroppable/MultipleDroppableApp.ts ================================================ import {DragDropManager, Draggable, Droppable} from '@dnd-kit/dom'; export default function App() { const root = document.createElement('div'); root.style.display = 'grid'; root.style.gridTemplateColumns = '1fr 1fr'; root.style.gap = '20px'; root.style.maxWidth = '500px'; root.style.margin = '0 auto'; const buttonWrapper = document.createElement('div'); buttonWrapper.style.display = 'flex'; buttonWrapper.style.alignItems = 'center'; buttonWrapper.style.justifyContent = 'center'; const button = document.createElement('button'); button.className = 'btn'; button.textContent = 'draggable'; buttonWrapper.appendChild(button); root.appendChild(buttonWrapper); const manager = new DragDropManager(); const droppableIds = ['A', 'B', 'C']; const dropzones: Record = {}; new Draggable({id: 'draggable', element: button}, manager); for (const id of droppableIds) { const dropzone = document.createElement('div'); dropzone.className = 'droppable'; dropzones[id] = dropzone; root.appendChild(dropzone); const droppable = new Droppable( { id, element: dropzone, effects: () => [ () => { if (droppable.isDropTarget) { dropzone.classList.add('active'); return () => dropzone.classList.remove('active'); } }, ], }, manager ); } manager.monitor.addEventListener('dragend', (event) => { if (event.canceled) return; const targetId = event.operation.target?.id as string; // Find which dropzone currently contains the button const currentParent = Object.entries(dropzones).find(([, el]) => el.contains(button) ); if (targetId && dropzones[targetId]) { if (!dropzones[targetId].contains(button)) { dropzones[targetId].appendChild(button); } } else if (currentParent) { buttonWrapper.appendChild(button); } }); return root; } ================================================ FILE: apps/stories-vanilla/stories/Sortable/Grid/Grid.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/html-vite'; import App from './GridSortableApp.ts'; import gridSortableSource from './GridSortableApp.ts?raw'; import {baseStyles, sortableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Sortable/Grid', }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', render: () => App(), parameters: { codesandbox: { files: { 'src/App.ts': gridSortableSource, 'src/styles.css': [baseStyles, sortableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-vanilla/stories/Sortable/Grid/GridSortableApp.ts ================================================ import {DragDropManager} from '@dnd-kit/dom'; import {Sortable} from '@dnd-kit/dom/sortable'; export default function App() { const wrapper = document.createElement('div'); wrapper.style.display = 'grid'; wrapper.style.gridTemplateColumns = 'repeat(auto-fill, 150px)'; wrapper.style.gridAutoRows = '150px'; wrapper.style.gridAutoFlow = 'dense'; wrapper.style.gap = '18px'; wrapper.style.padding = '0 30px'; wrapper.style.maxWidth = '900px'; wrapper.style.marginInline = 'auto'; wrapper.style.justifyContent = 'center'; const manager = new DragDropManager(); const items = createRange(20); for (const id of items) { const element = document.createElement('div'); element.classList.add('item'); element.textContent = String(id); element.style.height = '100%'; element.style.justifyContent = 'center'; wrapper.appendChild(element); const sortable = new Sortable( { id, element, index: id - 1, effects: () => [ () => { if (sortable.isDragging) { element.dataset.shadow = ''; return () => delete element.dataset.shadow; } }, ], }, manager ); } return wrapper; } function createRange(length: number) { return Array.from({length}, (_, i) => i + 1); } ================================================ FILE: apps/stories-vanilla/stories/Sortable/Horizontal/Horizontal.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/html-vite'; import App from './HorizontalSortableApp.ts'; import horizontalSortableSource from './HorizontalSortableApp.ts?raw'; import {baseStyles, sortableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Sortable/Horizontal list', }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', render: () => App(), parameters: { codesandbox: { files: { 'src/App.ts': horizontalSortableSource, 'src/styles.css': [baseStyles, sortableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-vanilla/stories/Sortable/Horizontal/HorizontalSortableApp.ts ================================================ import {DragDropManager} from '@dnd-kit/dom'; import {Sortable} from '@dnd-kit/dom/sortable'; export default function App() { const wrapper = document.createElement('div'); wrapper.style.display = 'inline-flex'; wrapper.style.flexDirection = 'row'; wrapper.style.alignItems = 'stretch'; wrapper.style.height = '180px'; wrapper.style.gap = '18px'; wrapper.style.padding = '0 30px'; const manager = new DragDropManager(); const items = createRange(10); for (const id of items) { const element = document.createElement('div'); element.classList.add('item'); element.textContent = String(id); element.style.aspectRatio = '1'; element.style.justifyContent = 'center'; wrapper.appendChild(element); const sortable = new Sortable( { id, element, index: id - 1, effects: () => [ () => { if (sortable.isDragging) { element.dataset.shadow = ''; return () => delete element.dataset.shadow; } }, ], }, manager ); } return wrapper; } function createRange(length: number) { return Array.from({length}, (_, i) => i + 1); } ================================================ FILE: apps/stories-vanilla/stories/Sortable/Sortable.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/html-vite'; import App from './SortableApp.ts'; import sortableSource from './SortableApp.ts?raw'; import {baseStyles, sortableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Sortable/Vertical list', }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', render: () => App(), parameters: { codesandbox: { files: { 'src/App.ts': sortableSource, 'src/styles.css': [baseStyles, sortableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-vanilla/stories/Sortable/SortableApp.ts ================================================ import {DragDropManager} from '@dnd-kit/dom'; import {Sortable} from '@dnd-kit/dom/sortable'; export default function App() { const list = document.createElement('ul'); list.className = 'list'; const manager = new DragDropManager(); const items = createRange(100); for (const id of items) { const li = document.createElement('li'); li.className = 'item'; li.textContent = String(id); list.appendChild(li); const sortable = new Sortable( { id, element: li, index: id - 1, effects: () => [ () => { if (sortable.isDragging) { li.dataset.shadow = ''; return () => delete li.dataset.shadow; } }, ], }, manager ); } return list; } function createRange(length: number) { return Array.from({length}, (_, i) => i + 1); } ================================================ FILE: apps/stories-vue/.storybook/main.ts ================================================ import {readFileSync} from 'fs'; import {dirname, join} from 'path'; import {mergeConfig} from 'vite'; import vue from '@vitejs/plugin-vue'; const sharedPreviewHead = readFileSync( join(__dirname, '..', '..', 'stories-shared', 'preview-head.html'), 'utf-8' ); export default { previewHead: (head: string) => `${sharedPreviewHead}\n${head}`, stories: ['../stories/**/*.stories.ts'], addons: [ getAbsolutePath('@storybook/addon-links'), getAbsolutePath('@vueless/storybook-dark-mode'), getAbsolutePath('@dnd-kit/storybook-addon-codesandbox'), ], framework: { name: getAbsolutePath('@storybook/vue3-vite'), options: {}, }, async viteFinal(config) { return mergeConfig(config, { plugins: [ vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.includes('-'), }, }, }), ], define: { 'process.env': {}, }, optimizeDeps: { exclude: ['@dnd-kit/*'], }, }); }, }; function getAbsolutePath(value) { return dirname(require.resolve(join(value, 'package.json'))); } ================================================ FILE: apps/stories-vue/.storybook/manager-head.html ================================================ ================================================ FILE: apps/stories-vue/.storybook/manager.ts ================================================ import {addons} from 'storybook/manager-api'; import {theme} from './theme'; addons.setConfig({ theme, showPanel: false, }); ================================================ FILE: apps/stories-vue/.storybook/preview.ts ================================================ import '@dnd-kit/stories-shared/register'; import {withCodeSandbox} from '@dnd-kit/storybook-addon-codesandbox/decorator-dom'; const preview = { decorators: [withCodeSandbox], parameters: { codesandbox: { template: 'vue-cli', files: { 'src/main.js': [ "import {createApp} from 'vue';", "import App from './App.vue';", "import './styles.css';", "", "createApp(App).mount('#app');", ].join('\n'), 'package.json': JSON.stringify( { name: 'dnd-kit-sandbox', version: '0.1.0', private: true, main: '/src/main.js', alias: { vue: 'vue/dist/vue.esm-bundler.js', }, scripts: { serve: 'vue-cli-service serve', build: 'vue-cli-service build', }, dependencies: { 'core-js': '^3.26.1', '@dnd-kit/abstract': 'beta', '@dnd-kit/dom': 'beta', '@dnd-kit/vue': 'beta', '@dnd-kit/helpers': 'beta', '@dnd-kit/collision': 'beta', 'vue': '^3.5.0', }, devDependencies: { '@vue/cli-plugin-babel': '^5.0.8', '@vue/cli-plugin-typescript': '^5.0.8', '@vue/cli-service': '^5.0.8', 'typescript': '^4.9.3', }, }, null, 2 ), }, mainFile: 'src/App.vue', }, darkMode: { stylePreview: true, }, options: { storySort: { order: [ 'Draggable', [ 'Basic setup', 'Drag handles', 'Drag overlay', ], 'Droppable', 'Sortable', [ 'Vertical list', 'Horizontal list', 'Grid', 'Multiple lists', ], ], }, }, }, }; export default preview; ================================================ FILE: apps/stories-vue/.storybook/theme.ts ================================================ import {create} from 'storybook/theming/create'; import {default as brandImage} from './assets/dnd-kit-banner.svg'; export const theme = create({ base: 'light', brandImage, appBg: '#F9F9F9', }); ================================================ FILE: apps/stories-vue/package.json ================================================ { "name": "@dnd-kit/stories-vue", "version": "0.0.0", "type": "module", "private": true, "scripts": { "dev": "storybook dev -p 6008", "build": "storybook build", "clean": "rm -rf .turbo && rm -rf node_modules", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui" }, "dependencies": { "@dnd-kit/abstract": "*", "@dnd-kit/dom": "*", "@dnd-kit/vue": "*", "@dnd-kit/helpers": "*", "@dnd-kit/stories-shared": "*", "@dnd-kit/storybook-addon-codesandbox": "*", "vue": "^3.5.0" }, "devDependencies": { "@playwright/test": "^1.58.2", "@storybook/addon-links": "^9.0.15", "@storybook/vue3-vite": "^9.0.15", "@vitejs/plugin-vue": "^5.0.0", "@vueless/storybook-dark-mode": "^9.0.6", "storybook": "^9.0.15", "typescript": "^5.7.3", "vite": "^6.0.0" } } ================================================ FILE: apps/stories-vue/playwright.config.ts ================================================ import {defineConfig} from '@playwright/test'; const CI = !!process.env.CI; export default defineConfig({ testDir: './tests', timeout: 15_000, expect: { timeout: 5_000, }, fullyParallel: true, retries: CI ? 2 : 1, reporter: CI ? 'html' : 'list', use: { baseURL: 'http://localhost:6008', actionTimeout: 5_000, }, projects: [ { name: 'chromium', use: {browserName: 'chromium'}, }, ], webServer: { command: 'npx http-server storybook-static --port 6008 --silent', port: 6008, reuseExistingServer: !CI, timeout: 120_000, }, }); ================================================ FILE: apps/stories-vue/raw.d.ts ================================================ declare module '*.vue?raw' { const content: string; export default content; } declare module '*.ts?raw' { const content: string; export default content; } ================================================ FILE: apps/stories-vue/stories/Draggable/DragHandles/DragHandles.stories.ts ================================================ import {h} from 'vue'; import type {Meta, StoryObj} from '@storybook/vue3-vite'; import DragHandlesApp from './DragHandlesApp.vue'; import dragHandlesSource from './DragHandlesApp.vue?raw'; import { baseStyles, draggableStyles, handleStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Draggable/Drag handles', component: DragHandlesApp, }; export default meta; type Story = StoryObj; export const Example: Story = { decorators: [ (story) => ({ setup() { return () => h('div', [ h('style', baseStyles), h('style', draggableStyles), h('style', handleStyles), h(story()), ]); }, }), ], parameters: { codesandbox: { files: { 'src/App.vue': dragHandlesSource, 'src/styles.css': [baseStyles, draggableStyles, handleStyles].join( '\n\n' ), }, }, }, }; ================================================ FILE: apps/stories-vue/stories/Draggable/DragHandles/DragHandlesApp.vue ================================================ ================================================ FILE: apps/stories-vue/stories/Draggable/DragOverlay/DragOverlay.stories.ts ================================================ import {h} from 'vue'; import type {Meta, StoryObj} from '@storybook/vue3-vite'; import DragOverlayApp from './DragOverlayApp.vue'; import dragOverlaySource from './DragOverlayApp.vue?raw'; import { baseStyles, draggableStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Draggable/Drag overlay', component: DragOverlayApp, }; export default meta; type Story = StoryObj; export const Example: Story = { decorators: [ (story) => ({ setup() { return () => h('div', [ h('style', baseStyles), h('style', draggableStyles), h(story()), ]); }, }), ], parameters: { codesandbox: { files: { 'src/App.vue': dragOverlaySource, 'src/styles.css': [baseStyles, draggableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-vue/stories/Draggable/DragOverlay/DragOverlayApp.vue ================================================ ================================================ FILE: apps/stories-vue/stories/Draggable/Draggable.stories.ts ================================================ import {h} from 'vue'; import type {Meta, StoryObj} from '@storybook/vue3-vite'; import DraggableApp from './DraggableApp.vue'; import draggableSource from './DraggableApp.vue?raw'; import { baseStyles, draggableStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Draggable/Basic setup', component: DraggableApp, }; export default meta; type Story = StoryObj; export const Example: Story = { decorators: [ (story) => ({ setup() { return () => h('div', [ h('style', baseStyles), h('style', draggableStyles), h(story()), ]); }, }), ], parameters: { codesandbox: { files: { 'src/App.vue': draggableSource, 'src/styles.css': [baseStyles, draggableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-vue/stories/Draggable/DraggableApp.vue ================================================ ================================================ FILE: apps/stories-vue/stories/Droppable/Droppable.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/vue3-vite'; import DroppableApp from './DroppableApp.vue'; import droppableSource from './DroppableApp.vue?raw'; import {baseStyles, draggableStyles, droppableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Droppable/Basic setup', component: DroppableApp, }; export default meta; type Story = StoryObj; export const Example: Story = { parameters: { codesandbox: { files: { 'src/App.vue': droppableSource, 'src/styles.css': [baseStyles, draggableStyles, droppableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-vue/stories/Droppable/DroppableApp.vue ================================================ ================================================ FILE: apps/stories-vue/stories/Droppable/MultipleDroppable/MultipleDroppable.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/vue3-vite'; import MultipleDroppableApp from './MultipleDroppableApp.vue'; import multipleDroppableSource from './MultipleDroppableApp.vue?raw'; import {baseStyles, draggableStyles, droppableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Droppable/Multiple drop targets', component: MultipleDroppableApp, }; export default meta; type Story = StoryObj; export const Example: Story = { parameters: { codesandbox: { files: { 'src/App.vue': multipleDroppableSource, 'src/styles.css': [baseStyles, draggableStyles, droppableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-vue/stories/Droppable/MultipleDroppable/MultipleDroppableApp.vue ================================================ ================================================ FILE: apps/stories-vue/stories/Sortable/Grid/Grid.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/vue3-vite'; import GridSortableApp from './GridSortableApp.vue'; import gridSortableSource from './GridSortableApp.vue?raw'; import {baseStyles, sortableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Sortable/Grid', }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', render: () => ({components: {GridSortableApp}, template: ''}), parameters: { codesandbox: { files: { 'src/App.vue': gridSortableSource, 'src/styles.css': [baseStyles, sortableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-vue/stories/Sortable/Grid/GridSortableApp.vue ================================================ ================================================ FILE: apps/stories-vue/stories/Sortable/Horizontal/Horizontal.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/vue3-vite'; import HorizontalSortableApp from './HorizontalSortableApp.vue'; import horizontalSortableSource from './HorizontalSortableApp.vue?raw'; import {baseStyles, sortableStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Sortable/Horizontal list', }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', render: () => ({components: {HorizontalSortableApp}, template: ''}), parameters: { codesandbox: { files: { 'src/App.vue': horizontalSortableSource, 'src/styles.css': [baseStyles, sortableStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-vue/stories/Sortable/Horizontal/HorizontalSortableApp.vue ================================================ ================================================ FILE: apps/stories-vue/stories/Sortable/MultipleLists/MultipleLists.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/vue3-vite'; import {h} from 'vue'; import MultipleListsApp from './MultipleListsApp.vue'; import multipleListsSource from './MultipleListsApp.vue?raw'; import sortableColumnSource from './SortableColumn.vue?raw'; import sortableItemSource from './SortableItem.vue?raw'; import {baseStyles, sortableStyles, multipleListsStyles} from '@dnd-kit/stories-shared/styles/sandbox'; const styles = [baseStyles, sortableStyles, multipleListsStyles].join('\n\n'); const meta: Meta = { title: 'Sortable/Multiple lists', component: MultipleListsApp, }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', render: () => ({ setup() { return () => h('div', [h('style', styles), h(MultipleListsApp)]); }, }), parameters: { codesandbox: { files: { 'src/App.vue': multipleListsSource, 'src/SortableColumn.vue': sortableColumnSource, 'src/SortableItem.vue': sortableItemSource, 'src/styles.css': styles, }, }, }, }; ================================================ FILE: apps/stories-vue/stories/Sortable/MultipleLists/MultipleListsApp.vue ================================================ ================================================ FILE: apps/stories-vue/stories/Sortable/MultipleLists/SortableColumn.vue ================================================ ================================================ FILE: apps/stories-vue/stories/Sortable/MultipleLists/SortableItem.vue ================================================ ================================================ FILE: apps/stories-vue/stories/Sortable/SortableApp.vue ================================================ ================================================ FILE: apps/stories-vue/stories/Sortable/SortableDragHandleApp.vue ================================================ ================================================ FILE: apps/stories-vue/stories/Sortable/Vertical/Vertical.stories.ts ================================================ import type {Meta, StoryObj} from '@storybook/vue3-vite'; import SortableApp from '../SortableApp.vue'; import sortableSource from '../SortableApp.vue?raw'; import SortableDragHandleApp from '../SortableDragHandleApp.vue'; import sortableDragHandleSource from '../SortableDragHandleApp.vue?raw'; import { baseStyles, handleStyles, sortableStyles, } from '@dnd-kit/stories-shared/styles/sandbox'; const meta: Meta = { title: 'Sortable/Vertical list', }; export default meta; type Story = StoryObj; export const BasicSetup: Story = { name: 'Basic setup', render: () => ({components: {SortableApp}, template: ''}), parameters: { codesandbox: { files: { 'src/App.vue': sortableSource, 'src/styles.css': [baseStyles, sortableStyles].join('\n\n'), }, }, }, }; export const WithDragHandle: Story = { name: 'Drag handle', render: () => ({components: {SortableDragHandleApp}, template: ''}), parameters: { codesandbox: { files: { 'src/App.vue': sortableDragHandleSource, 'src/styles.css': [baseStyles, sortableStyles, handleStyles].join('\n\n'), }, }, }, }; ================================================ FILE: apps/stories-vue/tests/draggable.spec.ts ================================================ import {draggableTests} from '../../stories-shared/tests/draggable.tests.ts'; draggableTests({ example: 'draggable-basic-setup--example', dragHandle: 'draggable-drag-handles--example', }); ================================================ FILE: apps/stories-vue/tests/droppable.spec.ts ================================================ import {droppableTests} from '../../stories-shared/tests/droppable.tests.ts'; droppableTests({ example: 'droppable-basic-setup--example', multipleDropTargets: 'droppable-multiple-drop-targets--example', }); ================================================ FILE: apps/stories-vue/tests/sortable-vertical.spec.ts ================================================ import {sortableVerticalTests} from '../../stories-shared/tests/sortable-vertical.tests.ts'; sortableVerticalTests({ basicSetup: 'sortable-vertical-list--basic-setup', withDragHandle: 'sortable-vertical-list--with-drag-handle', }); ================================================ FILE: config/typescript/base.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig", "display": "Default", "compilerOptions": { "composite": false, "declaration": true, "declarationMap": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "inlineSources": false, "isolatedModules": true, "noUnusedLocals": false, "noUnusedParameters": false, "preserveWatchOutput": true, "skipLibCheck": true, "strict": true, "allowImportingTsExtensions": true, "emitDeclarationOnly": true }, "exclude": ["node_modules"] } ================================================ FILE: config/typescript/react.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig", "display": "React", "extends": "./base.json", "compilerOptions": { "jsx": "react-jsx", "lib": ["dom", "ES2015"], "target": "es6", "module": "NodeNext", "moduleResolution": "NodeNext", "importHelpers": true } } ================================================ FILE: config/typescript/solid.json ================================================ { "$schema": "https://json-schema.org/tsconfig", "display": "Solid", "extends": "./base.json", "compilerOptions": { "lib": ["dom", "ES2022"], "target": "es6", "module": "NodeNext", "moduleResolution": "NodeNext", "jsx": "preserve", "jsxImportSource": "solid-js", "importHelpers": true, "sourceMap": true } } ================================================ FILE: config/typescript/svelte.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig", "display": "Svelte", "extends": "./base.json", "compilerOptions": { "lib": ["dom", "ES2022"], "target": "es6", "module": "NodeNext", "moduleResolution": "NodeNext", "importHelpers": true, "sourceMap": true, "verbatimModuleSyntax": true } } ================================================ FILE: config/typescript/vanilla.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig", "display": "Vanilla", "extends": "./base.json", "compilerOptions": { "lib": ["dom", "ES2015"], "target": "es6", "module": "NodeNext", "moduleResolution": "NodeNext", "importHelpers": true, "sourceMap": true } } ================================================ FILE: config/typescript/vue.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig", "display": "Vue", "extends": "./base.json", "compilerOptions": { "lib": ["dom", "ES2022"], "target": "es6", "module": "NodeNext", "moduleResolution": "NodeNext", "importHelpers": true, "sourceMap": true } } ================================================ FILE: package.json ================================================ { "private": true, "scripts": { "build": "turbo run build", "dev": "turbo run dev --concurrency 20", "test": "turbo run test", "test:e2e": "turbo run test:e2e", "lint": "turbo run lint", "clean": "turbo run clean && rm -rf node_modules", "format": "prettier --write \"**/*.{ts,tsx,md}\"", "changeset": "changeset", "version-packages": "changeset version", "release": "changeset publish", "version-packages:beta": "changeset version --snapshot beta", "release:beta": "changeset publish --snapshot --tag beta" }, "devDependencies": { "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "^2.29.2", "prettier": "^3.1.1", "turbo": "^2.5.2" }, "packageManager": "bun@1.1.12", "workspaces": [ "packages/*", "apps/*" ], "name": "dnd-kit-experimental" } ================================================ FILE: packages/abstract/CHANGELOG.md ================================================ # @dnd-kit/abstract ## 0.3.2 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.3.2 - @dnd-kit/state@0.3.2 ## 0.3.1 ### Patch Changes - [#1899](https://github.com/clauderic/dnd-kit/pull/1899) [`4341114`](https://github.com/clauderic/dnd-kit/commit/43411143063349caeded4f778923473624ce25cf) Thanks [@hanneskuettner](https://github.com/hanneskuettner)! - Fix modifiers passed to `DragDropProvider` being silently destroyed before they could take effect. An array reference comparison in the modifier lifecycle effect always evaluated to true, causing manager-level modifier instances to be destroyed and reassigned in a broken state on every drag start. - Updated dependencies []: - @dnd-kit/geometry@0.3.1 - @dnd-kit/state@0.3.1 ## 0.3.0 ### Minor Changes - [`6a59647`](https://github.com/clauderic/dnd-kit/commit/6a59647ebba2114b2e423f282ab25bf2ea40318d) Thanks [@clauderic](https://github.com/clauderic)! - Allow `plugins`, `sensors`, and `modifiers` to accept a function that receives the defaults, making it easy to extend or configure them without replacing the entire array. ```ts // Add a plugin alongside the defaults const manager = new DragDropManager({ plugins: (defaults) => [...defaults, MyPlugin], }); ``` ```tsx // Configure a default plugin in React [ ...defaults, Feedback.configure({dropAnimation: null}), ]} /> ``` Previously, passing `plugins`, `sensors`, or `modifiers` would replace the defaults entirely, requiring consumers to import and spread `defaultPreset`. The function form receives the default values as an argument, so consumers can add, remove, or configure individual entries without needing to know or maintain the full default list. ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.3.0 - @dnd-kit/state@0.3.0 ## 0.2.4 ### Patch Changes - [#1874](https://github.com/clauderic/dnd-kit/pull/1874) [`de27fbc`](https://github.com/clauderic/dnd-kit/commit/de27fbca9df12eece3cd53ccbbac34e0eaf113e1) Thanks [@clauderic](https://github.com/clauderic)! - Expose ergonomic type aliases for drag and drop event handlers: `CollisionEvent`, `BeforeDragStartEvent`, `DragStartEvent`, `DragMoveEvent`, `DragOverEvent`, and `DragEndEvent`. These types are re-exported from `@dnd-kit/dom` and `@dnd-kit/react` for convenience. - [#1866](https://github.com/clauderic/dnd-kit/pull/1866) [`be7cfe3`](https://github.com/clauderic/dnd-kit/commit/be7cfe3b6cf6a989aefd3e39fd145fe271942b3a) Thanks [@github-actions](https://github.com/apps/github-actions)! - Fix TypeScript type incompatibility when using abstract modifiers (`RestrictToVerticalAxis`, `RestrictToHorizontalAxis`, `SnapModifier`) with DOM or React `DragDropManager`. The `AxisModifier` and `SnapModifier` classes no longer over-constrain their generic manager type parameter. - Updated dependencies []: - @dnd-kit/geometry@0.2.4 - @dnd-kit/state@0.2.4 ## 0.2.3 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.2.3 - @dnd-kit/state@0.2.3 ## 0.2.2 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.2.2 - @dnd-kit/state@0.2.2 ## 0.2.1 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.2.1 - @dnd-kit/state@0.2.1 ## 0.2.0 ### Minor Changes - [#1821](https://github.com/clauderic/dnd-kit/pull/1821) [`e95a9c8`](https://github.com/clauderic/dnd-kit/commit/e95a9c8f448d6b339e0b6fd37546ac7cfdf18edb) Thanks [@clauderic](https://github.com/clauderic)! - - Added `ActivationController` and `ActivationConstraint` primitives for input activation orchestration. - Exported `ActivationConstraints` type for composing multiple constraints. ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.2.0 - @dnd-kit/state@0.2.0 ## 0.1.21 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.1.21 - @dnd-kit/state@0.1.21 ## 0.1.20 ### Patch Changes - Updated dependencies [[`98d4cd4`](https://github.com/clauderic/dnd-kit/commit/98d4cd4047c56589cdf21067526426717bba01c4), [`32448ff`](https://github.com/clauderic/dnd-kit/commit/32448ff11eb3e86a28fc8f6ef7a8a3761e092412)]: - @dnd-kit/state@0.1.20 - @dnd-kit/geometry@0.1.20 ## 0.1.19 ### Patch Changes - Updated dependencies [[`d848327`](https://github.com/clauderic/dnd-kit/commit/d848327b242c6714b36207071ad30e6b4183e865)]: - @dnd-kit/state@0.1.19 - @dnd-kit/geometry@0.1.19 ## 0.1.18 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.1.18 - @dnd-kit/state@0.1.18 ## 0.1.17 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.1.17 - @dnd-kit/state@0.1.17 ## 0.1.16 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.1.16 - @dnd-kit/state@0.1.16 ## 0.1.15 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.1.15 - @dnd-kit/state@0.1.15 ## 0.1.14 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.1.14 - @dnd-kit/state@0.1.14 ## 0.1.13 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.1.13 - @dnd-kit/state@0.1.13 ## 0.1.12 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.1.12 - @dnd-kit/state@0.1.12 ## 0.1.11 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.1.11 - @dnd-kit/state@0.1.11 ## 0.1.10 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.1.10 - @dnd-kit/state@0.1.10 ## 0.1.9 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.1.9 - @dnd-kit/state@0.1.9 ## 0.1.8 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.1.8 - @dnd-kit/state@0.1.8 ## 0.1.7 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.1.7 - @dnd-kit/state@0.1.7 ## 0.1.6 ### Patch Changes - [#1671](https://github.com/clauderic/dnd-kit/pull/1671) [`7ceb799`](https://github.com/clauderic/dnd-kit/commit/7ceb799c7d214bc8223ec845357a0040c28ae40e) Thanks [@github-actions](https://github.com/apps/github-actions)! - Fix shape snapshotting in DragOperation - Ensure shape is properly snapshotted when creating operation state snapshot - Updated dependencies [[`299389b`](https://github.com/clauderic/dnd-kit/commit/299389befcc747fe8d79231ba32f73afae88615e)]: - @dnd-kit/state@0.1.6 - @dnd-kit/geometry@0.1.6 ## 0.1.5 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.1.5 - @dnd-kit/state@0.1.5 ## 0.1.4 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.1.4 - @dnd-kit/state@0.1.4 ## 0.1.3 ### Patch Changes - [#1663](https://github.com/clauderic/dnd-kit/pull/1663) [`6c9a9ea`](https://github.com/clauderic/dnd-kit/commit/6c9a9ea060095884c90c72cd5d6b73820467ec29) Thanks [@github-actions](https://github.com/apps/github-actions)! - Prevent race conditions in `dragOperation` when `actions.stop()` is invoked before `actions.start()` has completed. - [#1663](https://github.com/clauderic/dnd-kit/pull/1663) [`1bef872`](https://github.com/clauderic/dnd-kit/commit/1bef8722d515079f998dc0608084e1d853e74d3a) Thanks [@github-actions](https://github.com/apps/github-actions)! - Improve drag operation control by: - Introducing `AbortController` for better operation lifecycle management - Remove `requestAnimationFram()` from `start()` action - Replacing boolean returns with proper abort control - Ensure proper cleanup of drag operations - Improving status handling and initialization checks - Making feedback plugin respect operation initialization state - Updated dependencies [[`8f91d91`](https://github.com/clauderic/dnd-kit/commit/8f91d9112608d2077c3b6c8fc939aa052606148c), [`2522836`](https://github.com/clauderic/dnd-kit/commit/2522836fdb80520913ea35d94c6558bf7784afc9), [`9a0edf6`](https://github.com/clauderic/dnd-kit/commit/9a0edf64cbde1bd761f3650e043b6612e61a5fab), [`a9db4c7`](https://github.com/clauderic/dnd-kit/commit/a9db4c73467d9eda9f95fe5b582948c9fc735f57)]: - @dnd-kit/state@0.1.3 - @dnd-kit/geometry@0.1.3 ## 0.1.2 ### Patch Changes - [#1658](https://github.com/clauderic/dnd-kit/pull/1658) [`4682570`](https://github.com/clauderic/dnd-kit/commit/4682570a6b80868af0e51b1bbbf902430117df43) Thanks [@github-actions](https://github.com/apps/github-actions)! - Fix handling of aborted drag operations across sensors. The `start` method now returns a boolean to indicate whether the operation was aborted, allowing sensors to properly clean up when a drag operation is prevented. This affects the Keyboard and Pointer sensors, ensuring they properly handle cases where `beforeDragStart` events are prevented. - [#1658](https://github.com/clauderic/dnd-kit/pull/1658) [`f8d69b0`](https://github.com/clauderic/dnd-kit/commit/f8d69b01f4cf53fc368ef1fca9188c313192928d) Thanks [@github-actions](https://github.com/apps/github-actions)! - Allow `actions.start()` to optionally receive a `source` as input. - [#1658](https://github.com/clauderic/dnd-kit/pull/1658) [`d04e9a2`](https://github.com/clauderic/dnd-kit/commit/d04e9a2879fb00f092c3f8280c8081a48eebf193) Thanks [@github-actions](https://github.com/apps/github-actions)! - Prevent starting a new drag operation while another one is active by adding a status check in the drag operation manager. This change throws an error if an attempt is made to start a drag operation while another one is in progress. - [#1658](https://github.com/clauderic/dnd-kit/pull/1658) [`ee55f58`](https://github.com/clauderic/dnd-kit/commit/ee55f582f92dc42cc6eea9ad7492fc782ca6455a) Thanks [@github-actions](https://github.com/apps/github-actions)! - Refactor the drag operation system to improve code organization and maintainability: - Split `dragOperation.ts` into multiple focused files: - `operation.ts` - Core drag operation logic - `status.ts` - Status management - `actions.ts` - Drag actions - Update imports and exports to reflect new file structure - Improve type definitions and exports - Updated dependencies [[`ee55f58`](https://github.com/clauderic/dnd-kit/commit/ee55f582f92dc42cc6eea9ad7492fc782ca6455a)]: - @dnd-kit/state@0.1.2 - @dnd-kit/geometry@0.1.2 ## 0.1.1 ### Patch Changes - [#1656](https://github.com/clauderic/dnd-kit/pull/1656) [`f13cbc9`](https://github.com/clauderic/dnd-kit/commit/f13cbc978229844770d3c8aa03135e4352ee2532) Thanks [@github-actions](https://github.com/apps/github-actions)! - Add a new `initialization-pending` status to the drag operation lifecycle. This status is set after a dragOperation is initiated but before the `beforedragstart` event fires, which allows consumers to prevent a drag operation from being initialized. This provides better control over the drag operation lifecycle and enables cancellation of drag operations before they are initialized. - Updated dependencies []: - @dnd-kit/geometry@0.1.1 - @dnd-kit/state@0.1.1 ## 0.1.0 ### Minor Changes - [#1650](https://github.com/clauderic/dnd-kit/pull/1650) [`00a33c9`](https://github.com/clauderic/dnd-kit/commit/00a33c99e777ab205a45309a4efc8b3560bafdaf) Thanks [@MateusJabour](https://github.com/MateusJabour)! - Adds new `data` property to `Collision` type ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.1.0 - @dnd-kit/state@0.1.0 ## 0.0.10 ### Patch Changes - Updated dependencies []: - @dnd-kit/geometry@0.0.10 - @dnd-kit/state@0.0.10 ## 0.0.9 ### Patch Changes - [#1600](https://github.com/clauderic/dnd-kit/pull/1600) [`e36d954`](https://github.com/clauderic/dnd-kit/commit/e36d95420148659ba78bdbefd3a0a24ec5d02b8f) Thanks [@github-actions](https://github.com/apps/github-actions)! - Added `nativeEvent` property to `dragstart`, `dragmove` and `dragend` events. This can be used to distinguish user triggered events from sensor triggered events, as user or plugin triggered events will typically not have an associated `event` attached. - [#1600](https://github.com/clauderic/dnd-kit/pull/1600) [`b7f1cf8`](https://github.com/clauderic/dnd-kit/commit/b7f1cf8f9e15a285c45f896e092f61001335cdff) Thanks [@github-actions](https://github.com/apps/github-actions)! - Fixed a bug in the `CollisionObserver` where the initial set of collisions when a drag operation is initiated were not being set and notified. - [#1600](https://github.com/clauderic/dnd-kit/pull/1600) [`3e629cc`](https://github.com/clauderic/dnd-kit/commit/3e629cc81dbaf9d112c4f1d2c10c75eb6779cf4e) Thanks [@github-actions](https://github.com/apps/github-actions)! - Added the option to trigger `move` actions that are not propagated to `dragmove` listeners. This can be useful when firing a `dragmove` action in response to another `dragmove` event to avoid an infinite loop. - [#1600](https://github.com/clauderic/dnd-kit/pull/1600) [`ce31da7`](https://github.com/clauderic/dnd-kit/commit/ce31da736ec5d4f48bab45430be7b57223d60ee7) Thanks [@github-actions](https://github.com/apps/github-actions)! - Added `dragOperation.shape.initial` to the list of dependencies that cause the `transform` and `modifiers` to be re-calculated. - Updated dependencies [[`60e7297`](https://github.com/clauderic/dnd-kit/commit/60e72979850bfe4cbb8e2b2e2b8e84bce9edc9f5), [`8ae7014`](https://github.com/clauderic/dnd-kit/commit/8ae70143bc404bff7678fa8e8390a640c16f2579)]: - @dnd-kit/geometry@0.0.9 - @dnd-kit/state@0.0.9 ## 0.0.8 ### Patch Changes - [#1598](https://github.com/clauderic/dnd-kit/pull/1598) [`c9716cf`](https://github.com/clauderic/dnd-kit/commit/c9716cf7b8b846faab451bd2f60c53c77d2d24ba) Thanks [@github-actions](https://github.com/apps/github-actions)! - Added `isDragging` and `isDropping` properties to `draggable` and `sortable` instances. - [#1598](https://github.com/clauderic/dnd-kit/pull/1598) [`3ea0d31`](https://github.com/clauderic/dnd-kit/commit/3ea0d314649b186bfe0524d50145625da13a8787) Thanks [@github-actions](https://github.com/apps/github-actions)! - Added optional `register` argument to instances of `Entity` to disable automatic registration of instances that have a manager supplied on initialization. - [#1597](https://github.com/clauderic/dnd-kit/pull/1597) [`3cf4db1`](https://github.com/clauderic/dnd-kit/commit/3cf4db126813ebe6ddfc025df5e42e9bfcfa9c38) Thanks [@clauderic](https://github.com/clauderic)! - Added the `registerEffect` method that can be invoked by sub-classes that extend the base `Plugin` class to register effects and automatically dispose of them when the plugin instance is destroyed. - Updated dependencies []: - @dnd-kit/geometry@0.0.8 - @dnd-kit/state@0.0.8 ## 0.0.7 ### Patch Changes - [#1592](https://github.com/clauderic/dnd-kit/pull/1592) [`c1dadef`](https://github.com/clauderic/dnd-kit/commit/c1dadef118f8f5f096d36dac314bfe317ea950ce) Thanks [@github-actions](https://github.com/apps/github-actions)! - Fire a cancelled `dragend` event when a drag operation is interrupted by the `DragDropManager` being destroyed during an operation. - [#1592](https://github.com/clauderic/dnd-kit/pull/1592) [`cef9b46`](https://github.com/clauderic/dnd-kit/commit/cef9b46c5ed017e6a601b1d0ee9d0f05b7bbd19f) Thanks [@github-actions](https://github.com/apps/github-actions)! - Fix global modifiers set on `DragDropManager` / `` being destroyed after the first drag operation. - Updated dependencies []: - @dnd-kit/geometry@0.0.7 - @dnd-kit/state@0.0.7 ## 0.0.6 ### Patch Changes - [#1553](https://github.com/clauderic/dnd-kit/pull/1553) [`984b5ab`](https://github.com/clauderic/dnd-kit/commit/984b5ab7bec3145dedb9c9b3b560ffbf7e54b919) Thanks [@chrisvxd](https://github.com/chrisvxd)! - Reconfigure the manager when the input changes. - [#1567](https://github.com/clauderic/dnd-kit/pull/1567) [`081b7f2`](https://github.com/clauderic/dnd-kit/commit/081b7f2a11da2aad8ce3da7f0579974415d1fdf0) Thanks [@chrisvxd](https://github.com/chrisvxd)! - Add source maps to output. - [#1454](https://github.com/clauderic/dnd-kit/pull/1454) [`a04d3f8`](https://github.com/clauderic/dnd-kit/commit/a04d3f88d380853b97585ab3b608561f7b02ce69) Thanks [@github-actions](https://github.com/apps/github-actions)! - Rework how collisions are detected and how the position of elements is observed using a new `PositionObserver`. - [#1454](https://github.com/clauderic/dnd-kit/pull/1454) [`a8542de`](https://github.com/clauderic/dnd-kit/commit/a8542de56d39c3cd3b6ef981172a0782454295b2) Thanks [@github-actions](https://github.com/apps/github-actions)! - Fix issues with `collisionPriority` not being respected. - [#1454](https://github.com/clauderic/dnd-kit/pull/1454) [`f7458d9`](https://github.com/clauderic/dnd-kit/commit/f7458d9dc32824dbea3a6d5dfb29236f19a2c073) Thanks [@github-actions](https://github.com/apps/github-actions)! - Fixed a bug where the `accept` function of `Droppable` was never invoked if the `draggable` did not have a `type` set. - [#1454](https://github.com/clauderic/dnd-kit/pull/1454) [`e70b29a`](https://github.com/clauderic/dnd-kit/commit/e70b29ae64837e424f7279c95112fb6e420c4dcc) Thanks [@github-actions](https://github.com/apps/github-actions)! - Make sure the generic for `DragDropManager` is passed through to `Entity` so that the `manager` reference on classes extending `Entity` is strongly typed. - [#1454](https://github.com/clauderic/dnd-kit/pull/1454) [`4d1a030`](https://github.com/clauderic/dnd-kit/commit/4d1a0306c920ae064eb5b30c4c02961f50460c84) Thanks [@github-actions](https://github.com/apps/github-actions)! - Make sure the cleanup function of effects is invoked when registering a new instance with the same `id` before the old instance has been unregistered. - [#1454](https://github.com/clauderic/dnd-kit/pull/1454) [`a5933d8`](https://github.com/clauderic/dnd-kit/commit/a5933d8607e63ed08818ffab43e858863cb35d47) Thanks [@github-actions](https://github.com/apps/github-actions)! - Move responsibility from `CollisionObserver` to `CollisionNotifier` to check if the previous collisions are equal to the next collisions. - [#1454](https://github.com/clauderic/dnd-kit/pull/1454) [`a5a556a`](https://github.com/clauderic/dnd-kit/commit/a5a556abfeec1d78effb3e047f529555e444c020) Thanks [@github-actions](https://github.com/apps/github-actions)! - Fixed React lifecycle regressions related to StrictMode. - [#1448](https://github.com/clauderic/dnd-kit/pull/1448) [`96f28ef`](https://github.com/clauderic/dnd-kit/commit/96f28ef86adf95e77540732d39033c7f3fb0fd04) Thanks [@lfades](https://github.com/lfades)! - Allow entities to receive a new id during the lifecycle of the entity - Updated dependencies [[`081b7f2`](https://github.com/clauderic/dnd-kit/commit/081b7f2a11da2aad8ce3da7f0579974415d1fdf0), [`b750c05`](https://github.com/clauderic/dnd-kit/commit/b750c05b4b14f5d9817dc07d974d40b74470e904), [`71dc39f`](https://github.com/clauderic/dnd-kit/commit/71dc39fb2ec21b9a680238a91be419c71ecabe86)]: - @dnd-kit/geometry@0.0.6 - @dnd-kit/state@0.0.6 ## 0.0.5 ### Patch Changes - [`e9be505`](https://github.com/clauderic/dnd-kit/commit/e9be5051b5c99e522fb6efd028d425220b171890) Thanks [@clauderic](https://github.com/clauderic)! - Fix lifecycle of local modifiers now that it's possible to initialize a Draggable instance without a manager instance. - Updated dependencies []: - @dnd-kit/geometry@0.0.5 - @dnd-kit/state@0.0.5 ## 0.0.4 ### Patch Changes - [#1443](https://github.com/clauderic/dnd-kit/pull/1443) [`2ccc27c`](https://github.com/clauderic/dnd-kit/commit/2ccc27c566b13d6de46719d0ad5978d655261177) Thanks [@clauderic](https://github.com/clauderic)! - Added `status` property to draggable instances to know the current status of a draggable instance. Useful to know if an instance is being dropped. - [#1443](https://github.com/clauderic/dnd-kit/pull/1443) [`e0d80f5`](https://github.com/clauderic/dnd-kit/commit/e0d80f59c733b3adcf1fc89d29aa80257e7edd98) Thanks [@clauderic](https://github.com/clauderic)! - Refactor the lifecycle to allow `manager` to be optional and provided later during the lifecycle of `draggable` / `droppable` / `sortable` instances. - [#1443](https://github.com/clauderic/dnd-kit/pull/1443) [`794cf2f`](https://github.com/clauderic/dnd-kit/commit/794cf2f4bdeeb57a197effb1df654c7c44cf34a3) Thanks [@clauderic](https://github.com/clauderic)! - Removed `options` and `options.register` from `Entity` base class. Passing an `undefined` manager when instantiating `Draggable` and `Droppable` now has the same effect. - Updated dependencies [[`a4d9150`](https://github.com/clauderic/dnd-kit/commit/a4d91500124698abf58355592913f84d438faa3d)]: - @dnd-kit/state@0.0.4 - @dnd-kit/geometry@0.0.4 ## 0.0.3 ### Patch Changes - [#1440](https://github.com/clauderic/dnd-kit/pull/1440) [`5ccd5e6`](https://github.com/clauderic/dnd-kit/commit/5ccd5e668fb8d736ec3c195116559cb5c5684e80) Thanks [@clauderic](https://github.com/clauderic)! - Add the ability for modifiers to be set dynamically on the `Draggable` instances - [#1440](https://github.com/clauderic/dnd-kit/pull/1440) [`886de33`](https://github.com/clauderic/dnd-kit/commit/886de33d0df851ebdcb3fcf2915f9623069b06d1) Thanks [@clauderic](https://github.com/clauderic)! - Introduced `SnapModifier` to snap to grid - Updated dependencies []: - @dnd-kit/geometry@0.0.3 - @dnd-kit/state@0.0.3 ## 0.0.2 ### Patch Changes - Updated dependencies [[`6c84308`](https://github.com/clauderic/dnd-kit/commit/6c84308b45c55ca1324a5c752b0ec117235da9e2)]: - @dnd-kit/state@0.0.2 - @dnd-kit/geometry@0.0.2 ================================================ FILE: packages/abstract/README.md ================================================ # @dnd-kit/abstract [![Stable release](https://img.shields.io/npm/v/@dnd-kit/abstract.svg)](https://npm.im/@dnd-kit/abstract) Abstract implementation of @dnd-kit, which can be extended by concrete implementation layers, such as @dnd-kit/dom. ## Overview This package provides the core abstractions and utilities for implementing drag and drop functionality. It serves as the foundation for building concrete implementation layers on top of @dnd-kit, such as the [DOM implementation layer](../dom). > [!NOTE] > This package is not meant to be used by most consumers, unless you are planning on building a concrete implementation layer on top of @dnd-kit. ## Core Concepts ### Entities The library defines two main entity types: - **Draggable**: Represents elements that can be dragged - **Droppable**: Represents elements that can receive dragged items ### Sensors Sensors are responsible for detecting and initiating drag operations. They handle user interactions and translate them into drag operations. The library provides: - Abstract `Sensor` base class - Support for multiple sensor types - Custom sensor configuration - Configurable sensor options ### Collision Detection The collision system provides: - Multiple collision detection strategies - Priority-based collision resolution - Custom collision detectors - Collision observation and management ### Plugins The plugin system allows extending core functionality: - Plugin-based architecture - Plugin configuration and descriptors - Plugin lifecycle management ### Modifiers Modifiers provide a way to transform or modify drag operations: - Abstract `Modifier` base class - Ability to modify coordinates and operations - Chainable modifier system - Configurable modifier options ## API ### Core Types - `DragDropManager`: Manages drag and drop state and operations - `Draggable`: Interface for draggable elements - `Droppable`: Interface for droppable elements - `Plugin`: Base class for plugins - `Sensor`: Abstract base class for sensors - `Modifier`: Abstract base class to modify drag operations ## Contributing This package is part of the dnd-kit monorepo. Please refer to the main repository's [contributing guidelines](/CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. ## License This package is licensed under the MIT License - see the [LICENSE](/LICENSE) file for details. ================================================ FILE: packages/abstract/package.json ================================================ { "name": "@dnd-kit/abstract", "version": "0.3.2", "type": "module", "main": "./index.cjs", "module": "./index.js", "types": "./index.d.ts", "sideEffects": false, "license": "MIT", "files": [ "LICENSE", "README.md", "index.js", "index.js.map", "index.d.ts", "index.d.cts", "index.cjs", "index.cjs.map", "modifiers.js", "modifiers.js.map", "modifiers.d.ts", "modifiers.d.cts", "modifiers.cjs", "modifiers.cjs.map" ], "exports": { ".": { "types": "./index.d.ts", "import": "./index.js", "require": "./index.cjs" }, "./modifiers": { "types": "./modifiers.d.ts", "import": "./modifiers.js", "require": "./modifiers.cjs" } }, "scripts": { "build": "bun build:core && bun build:modifiers", "build:core": "tsup src/core/index.ts", "build:modifiers": "tsup --entry.modifiers src/modifiers/index.ts", "dev": "bun build:core --watch & bun build:modifiers --watch", "test": "bun test", "lint": "TIMING=1 eslint src/**/*.ts* --fix", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, "dependencies": { "@dnd-kit/geometry": "^0.3.2", "@dnd-kit/state": "^0.3.2", "tslib": "^2.6.2" }, "devDependencies": { "@dnd-kit/eslint-config": "*", "eslint": "^8.38.0", "tsup": "8.3.0", "typescript": "^5.5.2" }, "publishConfig": { "access": "public" }, "repository": { "type": "git", "url": "https://github.com/clauderic/dnd-kit" } } ================================================ FILE: packages/abstract/src/core/collision/index.ts ================================================ export {CollisionObserver} from './observer.ts'; export {CollisionNotifier} from './notifier.ts'; export {sortCollisions} from './utilities.ts'; export * from './types.ts'; ================================================ FILE: packages/abstract/src/core/collision/notifier.ts ================================================ import {effects, untracked} from '@dnd-kit/state'; import {Entity} from '../entities/index.ts'; import {DragDropManager} from '../manager/index.ts'; import {CorePlugin} from '../plugins/index.ts'; import {defaultPreventable} from '../manager/events.ts'; import type {Collision} from './types.ts'; export class CollisionNotifier extends CorePlugin { constructor(manager: DragDropManager) { super(manager); const isEqual = (a: Collision[], b: Collision[]) => a.map(({id}) => id).join('') === b.map(({id}) => id).join(''); let previousCollisions: Collision[] = []; this.destroy = effects( () => { const {dragOperation, collisionObserver} = manager; if (dragOperation.status.initializing) { previousCollisions = []; collisionObserver.enable(); } }, () => { const {collisionObserver, monitor} = manager; const {collisions} = collisionObserver; if (collisionObserver.isDisabled()) { return; } if (Entity.pendingIdChanges) { return; } const event = defaultPreventable({ collisions, }); monitor.dispatch('collision', event); if (event.defaultPrevented) { return; } if (isEqual(collisions, previousCollisions)) { return; } else { previousCollisions = collisions; } const [firstCollision] = collisions; untracked(() => { if (firstCollision?.id !== manager.dragOperation.target?.id) { collisionObserver.disable(); manager.actions.setDropTarget(firstCollision?.id).then(() => { collisionObserver.enable(); }); } }); } ); } } ================================================ FILE: packages/abstract/src/core/collision/observer.ts ================================================ import {signal, untracked, type Signal, effects} from '@dnd-kit/state'; import type {Coordinates} from '@dnd-kit/geometry'; import type {DragDropManager} from '../manager/index.ts'; import type {Draggable, Droppable} from '../entities/index.ts'; import {Plugin} from '../plugins/index.ts'; import type {Collision, CollisionDetector, Collisions} from './types.ts'; import {sortCollisions} from './utilities.ts'; const DEFAULT_VALUE: Collisions = []; /** * Observes and manages collision detection between draggable and droppable elements. * * @template T - The type of draggable entities * @template U - The type of droppable entities * @template V - The type of drag drop manager * * @remarks * The CollisionObserver is responsible for: * - Computing collisions between draggable and droppable elements * - Maintaining a signal of current collisions * - Updating collision state based on drag operation changes */ export class CollisionObserver< T extends Draggable = Draggable, U extends Droppable = Droppable, V extends DragDropManager = DragDropManager, > extends Plugin { /** * Creates a new CollisionObserver instance. * * @param manager - The drag drop manager instance */ constructor(manager: V) { super(manager); this.computeCollisions = this.computeCollisions.bind(this); this.#collisions = signal(DEFAULT_VALUE); this.destroy = effects( () => { const collisions = this.computeCollisions(); const coordinates = untracked( () => this.manager.dragOperation.position.current ); if (collisions !== DEFAULT_VALUE) { const previousCoordinates = this.#previousCoordinates; this.#previousCoordinates = coordinates; if ( previousCoordinates && coordinates.x == previousCoordinates.x && coordinates.y == previousCoordinates.y ) { return; } } else { this.#previousCoordinates = undefined; } this.#collisions.value = collisions; }, () => { const {dragOperation} = this.manager; if (dragOperation.status.initialized) { this.forceUpdate(); } } ); } #previousCoordinates: Coordinates | undefined; /** * Forces an immediate update of collision detection. * * @param immediate - If true, updates collisions immediately. If false, resets previous coordinates. */ public forceUpdate(immediate = true) { untracked(() => { if (immediate) { this.#collisions.value = this.computeCollisions(); } else { this.#previousCoordinates = undefined; } }); } /** * Computes collisions between draggable and droppable elements. * * @param entries - Optional array of droppable elements to check. If not provided, uses all registered droppables. * @param collisionDetector - Optional custom collision detector function * @returns Array of detected collisions, sorted by priority */ public computeCollisions( entries?: Droppable[], collisionDetector?: CollisionDetector ) { const {registry, dragOperation} = this.manager; const {source, shape, status} = dragOperation; if (!status.initialized || !shape) { return DEFAULT_VALUE; } const collisions: Collision[] = []; const potentialTargets = []; for (const entry of entries ?? registry.droppables) { if (entry.disabled) { continue; } if (source && !entry.accepts(source)) { continue; } const detectCollision = collisionDetector ?? entry.collisionDetector; if (!detectCollision) { continue; } potentialTargets.push(entry); // Force collisions to be recomputed when the shape changes void entry.shape; const collision = untracked(() => detectCollision({ droppable: entry, dragOperation, }) ); if (collision) { if (entry.collisionPriority != null) { collision.priority = entry.collisionPriority; } collisions.push(collision); } } if (potentialTargets.length === 0) { return DEFAULT_VALUE; } collisions.sort(sortCollisions); return collisions; } /** * Gets the current collisions signal value. */ public get collisions() { return this.#collisions.value; } #collisions: Signal; } ================================================ FILE: packages/abstract/src/core/collision/types.ts ================================================ import type {DragOperation} from '../manager/index.ts'; import type { Draggable, Droppable, UniqueIdentifier, } from '../entities/index.ts'; /** * Priority levels for collision detection. * * @remarks * Higher priority collisions take precedence over lower priority ones. * Custom numeric priorities can also be used for fine-grained control. */ export enum CollisionPriority { /** Lowest priority level */ Lowest, /** Low priority level */ Low, /** Normal priority level */ Normal, /** High priority level */ High, /** Highest priority level */ Highest, } /** * Types of collision detection. * * @remarks * Different collision types can be used to implement various * drag and drop behaviors and visual feedback. */ export enum CollisionType { /** Basic collision detection */ Collision, /** Shape-based intersection detection */ ShapeIntersection, /** Pointer-based intersection detection */ PointerIntersection, } /** * Represents a detected collision between a draggable and droppable. * * @remarks * Contains information about the collision type, priority, and * additional data that can be used for custom behaviors. */ export interface Collision { /** Unique identifier of the droppable involved in the collision */ id: UniqueIdentifier; /** Priority of the collision */ priority: CollisionPriority | number; /** Type of collision detected */ type: CollisionType; /** Numeric value representing the collision strength or overlap */ value: number; /** Additional data associated with the collision */ data?: Record; } /** Array of detected collisions */ export type Collisions = Collision[]; /** * Input for collision detection functions. * * @template T - The type of draggable entities * @template U - The type of droppable entities */ export interface CollisionDetectorInput< T extends Draggable = Draggable, U extends Droppable = Droppable, > { /** The droppable to check for collisions */ droppable: U; /** The current drag operation state */ dragOperation: DragOperation; } /** * Function type for detecting collisions between draggables and droppables. * * @template T - The type of draggable entities * @template U - The type of droppable entities * @param input - The collision detection input * @returns A collision object if detected, null otherwise */ export type CollisionDetector = < T extends Draggable = Draggable, U extends Droppable = Droppable, >( input: CollisionDetectorInput ) => Collision | null; ================================================ FILE: packages/abstract/src/core/collision/utilities.ts ================================================ import {Collision} from './types.ts'; /** * Sort collisions from greatest to smallest priority * Collisions of equal priority are sorted from greatest to smallest value */ export function sortCollisions(a: Collision, b: Collision) { if (a.priority === b.priority) { if (a.type === b.type) { return b.value - a.value; } return b.type - a.type; } return b.priority - a.priority; } ================================================ FILE: packages/abstract/src/core/entities/draggable/draggable.ts ================================================ import {derived, reactive} from '@dnd-kit/state'; import type {Alignment} from '@dnd-kit/geometry'; import {Entity} from '../entity/index.ts'; import type {EntityInput, Data, Type} from '../entity/index.ts'; import type {Modifiers} from '../../modifiers/index.ts'; import type {DragDropManager} from '../../manager/index.ts'; import type {Sensors} from '../../sensors/sensor.ts'; import type { Plugins, PluginConstructor, PluginDescriptor, } from '../../plugins/index.ts'; import {descriptor as toDescriptor} from '../../plugins/index.ts'; /** * Input configuration for creating a draggable entity. * * @template T - The type of data associated with the draggable * * @remarks * Extends the base entity input with draggable-specific configuration: * - Type for categorization * - Sensors for handling drag interactions * - Modifiers for transforming drag behavior * - Alignment for positioning */ export interface Input extends EntityInput { type?: Type; sensors?: Sensors; modifiers?: Modifiers; alignment?: Alignment; plugins?: Plugins; } /** * Possible status values for a draggable entity. * * @remarks * - idle: Not being dragged * - dragging: Currently being dragged * - dropping: Currently being dropped */ export type DraggableStatus = 'idle' | 'dragging' | 'dropping'; /** * Represents an entity that can be dragged in a drag and drop operation. * * @template T - The type of data associated with the draggable * @template U - The type of drag and drop manager * * @remarks * This class extends the base Entity class with draggable-specific functionality: * - Type-based categorization * - Sensor-based interaction handling * - Modifier-based behavior transformation * - Status tracking during drag operations */ export class Draggable< T extends Data = Data, U extends DragDropManager = DragDropManager, > extends Entity { constructor( {modifiers, type, sensors, plugins, effects, ...input}: Input, manager: U | undefined ) { super( { ...input, effects: () => [ ...(effects?.() ?? []), () => { const {manager, plugins} = this; if (!manager || !plugins) return; for (const entry of plugins) { const {plugin} = toDescriptor(entry); manager.registry.plugins.register(plugin); } }, ], }, manager ); this.type = type; this.sensors = sensors; this.modifiers = modifiers; this.alignment = input.alignment; this.plugins = plugins; } /** The type of the draggable entity */ @reactive public accessor type: Type | undefined; /** The sensors associated with the draggable entity */ public sensors: Sensors | undefined; /** The modifiers associated with the draggable entity */ @reactive public accessor modifiers: Modifiers | undefined; /** The alignment of the draggable entity */ public alignment: Alignment | undefined; /** Per-entity plugin configuration descriptors */ public plugins: Plugins | undefined; /** * Look up per-entity options for a given plugin constructor. */ public pluginConfig

    ( plugin: P ): PluginDescriptor['options'] | undefined { if (!this.plugins) return undefined; for (const entry of this.plugins) { const desc = toDescriptor(entry); if (desc.plugin === plugin) return desc.options; } return undefined; } /** The current status of the draggable entity */ @reactive public accessor status: DraggableStatus = this.isDragSource ? 'dragging' : 'idle'; /** * Checks if the draggable entity is currently being dropped. * * @returns true if the entity is being dropped and is the drag source */ @derived public get isDropping() { return this.status === 'dropping' && this.isDragSource; } /** * Checks if the draggable entity is currently being dragged. * * @returns true if the entity is being dragged and is the drag source */ @derived public get isDragging() { return this.status === 'dragging' && this.isDragSource; } /** * Checks if the draggable entity is the source of the current drag operation. * * @returns true if the entity's ID matches the current drag operation's source ID */ @derived public get isDragSource() { return this.manager?.dragOperation.source?.id === this.id; } } ================================================ FILE: packages/abstract/src/core/entities/draggable/index.ts ================================================ export {Draggable} from './draggable.ts'; export type {Input as DraggableInput} from './draggable.ts'; ================================================ FILE: packages/abstract/src/core/entities/droppable/droppable.ts ================================================ import {derived, effects, reactive, type Effect} from '@dnd-kit/state'; import type {Shape} from '@dnd-kit/geometry'; import {Entity} from '../entity/index.ts'; import type {EntityInput, Data, Type} from '../entity/index.ts'; import { CollisionPriority, type CollisionDetector, } from '../../collision/index.ts'; import type {DragDropManager} from '../../manager/index.ts'; import {Draggable} from '../draggable/draggable.ts'; /** * Input configuration for creating a droppable entity. * * @template T - The type of data associated with the droppable * * @remarks * Extends the base entity input with droppable-specific configuration: * - Accept rules for determining compatible draggables * - Collision detection configuration * - Type for categorization */ export interface Input extends EntityInput { /** Types of draggables that can be dropped here, or a function to determine compatibility */ accept?: Type | Type[] | ((source: Draggable) => boolean); /** Priority for collision detection */ collisionPriority?: CollisionPriority | number; /** Detector for determining collisions with draggables */ collisionDetector: CollisionDetector; /** Type for categorization */ type?: Type; } /** * Represents an entity that can receive draggable items in a drag and drop operation. * * @template T - The type of data associated with the droppable * @template U - The type of drag and drop manager * * @remarks * This class extends the base Entity class with droppable-specific functionality: * - Type-based acceptance rules * - Collision detection * - Shape tracking * - Target status tracking */ export class Droppable< T extends Data = Data, U extends DragDropManager = DragDropManager, > extends Entity { constructor( {accept, collisionDetector, collisionPriority, type, ...input}: Input, manager: U | undefined ) { super(input, manager); this.accept = accept; this.collisionDetector = collisionDetector; this.collisionPriority = collisionPriority; this.type = type; } /** * Types of draggables that can be dropped here, or a function to determine compatibility. * * @remarks * If undefined, all draggables are accepted. * If a function, it determines compatibility based on the draggable. * If a type or array of types, only draggables of matching types are accepted. */ @reactive public accessor accept: | Type | Type[] | ((draggable: Draggable) => boolean) | undefined; /** The type of the droppable entity */ @reactive public accessor type: Type | undefined; /** * Checks whether or not the droppable accepts a given draggable. * * @param draggable - The draggable to check * @returns true if the draggable can be dropped here */ public accepts(draggable: Draggable): boolean { const {accept} = this; if (!accept) { return true; } if (typeof accept === 'function') { return accept(draggable); } if (!draggable.type) { return false; } if (Array.isArray(accept)) { return accept.includes(draggable.type); } return draggable.type === accept; } /** The collision detector for this droppable */ @reactive public accessor collisionDetector: CollisionDetector; /** The collision priority for this droppable */ @reactive public accessor collisionPriority: CollisionPriority | number | undefined; /** The current shape of this droppable */ @reactive public accessor shape: Shape | undefined; /** * Checks if this droppable is the current drop target. * * @returns true if this droppable's ID matches the current drag operation's target ID */ @derived public get isDropTarget() { return this.manager?.dragOperation.target?.id === this.id; } } ================================================ FILE: packages/abstract/src/core/entities/droppable/index.ts ================================================ export {Droppable} from './droppable.ts'; export type {Input as DroppableInput} from './droppable.ts'; ================================================ FILE: packages/abstract/src/core/entities/entity/entity.ts ================================================ import { batch, CleanupFunction, reactive, signal, type Signal, type Effect, } from '@dnd-kit/state'; import {DragDropManager} from '../../manager/index.ts'; import type {Data, UniqueIdentifier} from './types.ts'; export interface Input { /** * The unique identifier of the entity. */ id: UniqueIdentifier; /** * Optional data associated with the entity. */ data?: T; /** * Whether the entity should initially be disabled. * @default false */ disabled?: boolean; /** * Whether the entity should be automatically registered with the manager when it is created. * @default true */ register?: boolean; /** * An array of effects that are set up when the entity is registered and cleaned up when it is unregistered. */ effects?(): Effect[]; } /** * The `Entity` class is an abstract representation of a distinct unit in the drag and drop system. * It is a base class that other concrete classes like `Draggable` and `Droppable` can extend. * * @template T - The type of data associated with the entity. */ export class Entity< T extends Data = Data, U extends DragDropManager = DragDropManager, > { static pendingIdChanges: Map | null = null; static #flushIdChanges() { const changes = Entity.pendingIdChanges; Entity.pendingIdChanges = null; if (changes) { batch(() => { for (const [entity, id] of changes) { entity.#idSignal.value = id; } }); } } /** * Creates a new instance of the `Entity` class. * * @param input - An object containing the initial properties of the entity. * @param manager - The manager that controls the drag and drop operations. */ constructor(input: Input, manager: U | undefined) { const {effects, id, data = {} as T, disabled = false, register = true} = input; let previousId = id; this.#idSignal = signal(id); this.manager = manager; this.data = data; this.disabled = disabled; this.effects = () => [ () => { const {id, manager} = this; if (id === previousId) { return; } previousId = id; manager?.registry.register(this); return () => manager?.registry.unregister(this); }, ...(effects?.() ?? []), ]; this.register = this.register.bind(this); this.unregister = this.unregister.bind(this); this.destroy = this.destroy.bind(this); if (manager && register) { queueMicrotask(this.register); } } /** * The manager that controls the drag and drop operations. */ @reactive public accessor manager: U | undefined; /** * The unique identifier of the entity. * * Setting this property defers the signal update to a microtask, * batching multiple id changes together atomically. This ensures * that when entities swap ids (e.g. during sorting with virtualization), * all registry updates happen in a single transaction. */ #idSignal: Signal; public get id(): UniqueIdentifier { const signalValue = this.#idSignal.value; return Entity.pendingIdChanges?.get(this) ?? signalValue; } public set id(value: UniqueIdentifier) { const current = Entity.pendingIdChanges?.get(this) ?? this.#idSignal.peek(); if (value === current) return; if (!Entity.pendingIdChanges) { Entity.pendingIdChanges = new Map(); queueMicrotask(() => Entity.#flushIdChanges()); } Entity.pendingIdChanges.set(this, value); } /** * The data associated with the entity. */ @reactive public accessor data: T; /** * A boolean indicating whether the entity is disabled. */ @reactive public accessor disabled: boolean; /** * An array of effects that are applied to the entity. */ public effects: () => Effect[]; /** * A method that registers the entity with the manager. * @returns CleanupFunction | void */ public register(): CleanupFunction | void { return this.manager?.registry.register(this); } /** * A method that unregisters the entity from the manager. * @returns void */ public unregister(): void { this.manager?.registry.unregister(this); } /** * A method that cleans up the entity when it is no longer needed. * @returns void */ public destroy(): void { this.manager?.registry.unregister(this); } } ================================================ FILE: packages/abstract/src/core/entities/entity/index.ts ================================================ export {Entity} from './entity.ts'; export type {Input as EntityInput} from './entity.ts'; export type {Data, Type, UniqueIdentifier} from './types.ts'; export {EntityRegistry} from './registry.ts'; ================================================ FILE: packages/abstract/src/core/entities/entity/registry.ts ================================================ import {effects, signal} from '@dnd-kit/state'; import type {Entity} from './entity.ts'; import type {UniqueIdentifier} from './types.ts'; /** * Reactive class representing a registry for entities. * @template T - The type of entries that the registry manages, * for example, `Draggable` or `Droppable` entities. */ export class EntityRegistry { private map = signal>(new Map()); private cleanupFunctions = new WeakMap void>(); /** * Iterator for the EntityRegistry class. * @returns An iterator for the values in the map. */ public [Symbol.iterator]() { return this.map.peek().values(); } public get value() { return this.map.value.values(); } /** * Checks if a entity with the given identifier exists in the registry. * @param identifier - The unique identifier of the entity. * @returns True if the entity exists, false otherwise. */ public has(identifier: UniqueIdentifier): boolean { return this.map.value.has(identifier); } /** * Retrieves a entity from the registry using its identifier. * @param identifier - The unique identifier of the entity. * @returns The entity if it exists, undefined otherwise. */ public get(identifier: UniqueIdentifier): T | undefined { return this.map.value.get(identifier); } /** * Registers a entity in the registry. * @param key - The unique identifier of the entity. * @param value - The entity to register. * @returns A function that unregisters the entity. */ public register = (key: UniqueIdentifier, value: T) => { const current = this.map.peek(); const currentValue = current.get(key); const unregister = () => this.unregister(key, value); if (currentValue === value) return unregister; if (currentValue) { if (currentValue.id === key) { const cleanup = this.cleanupFunctions.get(currentValue); cleanup?.(); this.cleanupFunctions.delete(currentValue); } } const updatedMap = new Map(current); // Remove ghost registrations: if this entity exists at a different key // (stale entry from before its id changed), clean it up for (const [existingKey, existingValue] of current) { if (existingValue === value && existingKey !== key) { updatedMap.delete(existingKey); break; } } updatedMap.set(key, value); this.map.value = updatedMap; const cleanup = effects(...value.effects()); this.cleanupFunctions.set(value, cleanup); return unregister; }; /** * Unregisters an entity from the registry. * @param key - The unique identifier of the entity. * @param value - The entity instance to unregister. */ public unregister = (key: UniqueIdentifier, value: T) => { const current = this.map.peek(); if (current.get(key) !== value) { return; } const cleanup = this.cleanupFunctions.get(value); cleanup?.(); this.cleanupFunctions.delete(value); const updatedMap = new Map(current); updatedMap.delete(key); this.map.value = updatedMap; }; /** * Destroys all entries in the registry and clears the registry. */ public destroy() { for (const entry of this) { const cleanup = this.cleanupFunctions.get(entry); cleanup?.(); entry.destroy(); } this.map.value = new Map(); } } ================================================ FILE: packages/abstract/src/core/entities/entity/types.ts ================================================ /** * Type representing arbitrary data associated with an entity. * * @remarks * This type is used to store additional information about entities * that can be accessed during drag and drop operations. */ export type Data = Record; /** * Type representing a unique identifier for an entity. * * @remarks * This type is used to uniquely identify draggable and droppable entities * within the drag and drop system. */ export type UniqueIdentifier = string | number; /** * Type representing the type of an entity. * * @remarks * This type is used to categorize entities and can be used to * implement type-based filtering or matching. */ export type Type = Symbol | string | number; ================================================ FILE: packages/abstract/src/core/entities/index.ts ================================================ export * from './entity/index.ts'; export * from './draggable/index.ts'; export * from './droppable/index.ts'; ================================================ FILE: packages/abstract/src/core/index.ts ================================================ export { DragDropManager, DragOperationStatus, resolveCustomizable, } from './manager/index.ts'; export type { DragDropManagerInput, Customizable, DragActions, DragDropEventMap, DragDropEventHandlers, CollisionEvent, BeforeDragStartEvent, DragStartEvent, DragMoveEvent, DragOverEvent, DragEndEvent, DragOperation, Renderer, } from './manager/index.ts'; export { CollisionPriority, CollisionType, sortCollisions, } from './collision/index.ts'; export type {Collision, CollisionDetector} from './collision/index.ts'; export {Modifier} from './modifiers/index.ts'; export type {Modifiers, ModifierConstructor} from './modifiers/index.ts'; export {Draggable, Droppable} from './entities/index.ts'; export type { Data, DraggableInput, DroppableInput, Entity, Type, UniqueIdentifier, } from './entities/index.ts'; export { Plugin, PluginRegistry, CorePlugin, configure, configurator, descriptor, } from './plugins/index.ts'; export type { Plugins, PluginConstructor, PluginDescriptor, PluginOptions, } from './plugins/index.ts'; export { ActivationConstraint, ActivationController, Sensor, } from './sensors/index.ts'; export type { ActivationConstraints, Sensors, SensorConstructor, SensorDescriptor, SensorOptions, } from './sensors/index.ts'; ================================================ FILE: packages/abstract/src/core/manager/actions.ts ================================================ import type {Coordinates} from '@dnd-kit/geometry'; import {batch, effect, untracked} from '@dnd-kit/state'; import type { Draggable, Droppable, UniqueIdentifier, } from '../entities/index.ts'; import type {DragDropManager} from './manager.ts'; import {defaultPreventable} from './events.ts'; import {StatusValue} from './status.ts'; /** * Provides actions for controlling drag and drop operations. * * @template T - The type of draggable entities * @template U - The type of droppable entities * @template V - The type of drag and drop manager */ export class DragActions< T extends Draggable, U extends Droppable, V extends DragDropManager, > { /** * Creates a new instance of drag actions. * * @param manager - The drag and drop manager instance */ constructor(private readonly manager: V) {} /** * Sets the source of the drag operation. * * @param source - The draggable entity or its unique identifier */ setDragSource(source: T | UniqueIdentifier) { const {dragOperation} = this.manager; dragOperation.sourceIdentifier = typeof source === 'string' || typeof source === 'number' ? source : source.id; } /** * Sets the target of the drop operation. * * @param identifier - The unique identifier of the droppable entity or null/undefined * @returns A promise that resolves to true if the drop was prevented */ setDropTarget( identifier: UniqueIdentifier | null | undefined ): Promise { return untracked(() => { const {dragOperation} = this.manager; const id = identifier ?? null; if (dragOperation.targetIdentifier === id) { return Promise.resolve(false); } dragOperation.targetIdentifier = id; const event = defaultPreventable({ operation: dragOperation.snapshot(), }); if (dragOperation.status.dragging) { this.manager.monitor.dispatch('dragover', event); } return this.manager.renderer.rendering.then(() => event.defaultPrevented); }); } /** * Starts a new drag operation. * * @param args - Configuration for the drag operation * @param args.event - The event that initiated the drag * @param args.source - The source draggable entity or its identifier * @param args.coordinates - The initial coordinates of the drag * @returns true if the drag operation started successfully * @throws {Error} If there is no drag source or another operation is active */ start(args: { /** The event that initiated the drag. */ event?: Event; /** The source draggable entity or its identifier. */ source?: T | UniqueIdentifier; /** The initial coordinates of the drag. */ coordinates: Coordinates; }): AbortController { return untracked(() => { const {dragOperation} = this.manager; if (args.source != null) { this.setDragSource(args.source); } const sourceInstance = dragOperation.source; if (!sourceInstance) { throw new Error('Cannot start a drag operation without a drag source'); } if (!dragOperation.status.idle) { throw new Error( 'Cannot start a drag operation while another is active' ); } const controller = new AbortController(); const {event: nativeEvent, coordinates} = args; batch(() => { dragOperation.status.set(StatusValue.InitializationPending); dragOperation.shape = null; dragOperation.canceled = false; dragOperation.activatorEvent = nativeEvent ?? null; dragOperation.position.reset(coordinates); }); const beforeStartEvent = defaultPreventable({ operation: dragOperation.snapshot(), }); this.manager.monitor.dispatch('beforedragstart', beforeStartEvent); if (beforeStartEvent.defaultPrevented) { dragOperation.reset(); controller.abort(); return controller; } dragOperation.status.set(StatusValue.Initializing); dragOperation.controller = controller; this.manager.renderer.rendering.then(() => { if (controller.signal.aborted) return; const {status} = dragOperation; if (status.current !== StatusValue.Initializing) return; dragOperation.status.set(StatusValue.Dragging); this.manager.monitor.dispatch('dragstart', { nativeEvent, operation: dragOperation.snapshot(), cancelable: false, }); }); return controller; }); } /** * Moves the dragged entity to a new position. * * @param args - Configuration for the move operation * @param args.by - Relative coordinates to move by * @param args.to - Absolute coordinates to move to * @param args.event - The event that triggered the move * @param args.cancelable - Whether the move can be canceled * @param args.propagate - Whether to dispatch dragmove events */ move(args: { /** The relative coordinates to move by. */ by?: Coordinates; /** The absolute coordinates to move to. */ to?: Coordinates; /** The event that triggered the move. */ event?: Event; /** Whether the move can be canceled. */ cancelable?: boolean; /** Whether to propagate the dragmove event to the manager. */ propagate?: boolean; }): void { return untracked(() => { const {dragOperation} = this.manager; const {status, controller} = dragOperation; if (!status.dragging || !controller || controller.signal.aborted) { return; } const event = defaultPreventable( { nativeEvent: args.event, operation: dragOperation.snapshot(), by: args.by, to: args.to, }, args.cancelable ?? true ); if (args.propagate ?? true) { this.manager.monitor.dispatch('dragmove', event); } queueMicrotask(() => { if (event.defaultPrevented) { return; } const coordinates = args.to ?? { x: dragOperation.position.current.x + (args.by?.x ?? 0), y: dragOperation.position.current.y + (args.by?.y ?? 0), }; dragOperation.position.current = coordinates; }); }); } /** * Stops the current drag operation. * * @param args - Configuration for stopping the operation * @param args.event - The event that triggered the stop * @param args.canceled - Whether the operation was canceled * @remarks * This method: * - Dispatches a dragend event * - Allows suspension of the operation * - Handles cleanup of the operation state */ stop( args: { /** * The event that triggered the stop. */ event?: Event; /** * Whether the operation was canceled. * * @default false */ canceled?: boolean; } = {} ): void { return untracked(() => { const {dragOperation} = this.manager; const {controller} = dragOperation; if (!controller || controller.signal.aborted) return; let promise: Promise | undefined; const suspend = () => { const output = { resume: () => {}, abort: () => {}, }; promise = new Promise((resolve, reject) => { output.resume = resolve; output.abort = reject; }); return output; }; controller.abort(); const end = () => { this.manager.renderer.rendering.then(() => { dragOperation.status.set(StatusValue.Dropped); const dropping = untracked( () => dragOperation.source?.status === 'dropping' ); const cleanup = () => { if (dragOperation.controller === controller) { dragOperation.controller = undefined; } dragOperation.reset(); }; if (dropping) { const {source} = dragOperation; // Wait until the source has finished dropping before resetting the operation const dispose = effect(() => { if (source?.status === 'idle') { dispose(); cleanup(); } }); } else { this.manager.renderer.rendering.then(cleanup); } }); }; dragOperation.canceled = args.canceled ?? false; this.manager.monitor.dispatch('dragend', { nativeEvent: args.event, operation: dragOperation.snapshot(), canceled: args.canceled ?? false, suspend, }); if (promise) { promise.then(end).catch(() => dragOperation.reset()); } else { end(); } }); } } ================================================ FILE: packages/abstract/src/core/manager/events.ts ================================================ import type {Coordinates} from '@dnd-kit/geometry'; import type {Draggable, Droppable} from '../entities/index.ts'; import type {Collisions} from '../collision/index.ts'; import type {DragDropManager} from './manager.ts'; import type {DragOperationSnapshot} from './operation.ts'; /** Base type for event handler functions */ export type Events = Record void>; /** * Extends an event type with preventable functionality. * * @template T - The base event type */ export type Preventable = T & { /** Whether the event can be canceled */ cancelable: boolean; /** Whether the default action was prevented */ defaultPrevented: boolean; /** Prevents the default action of the event */ preventDefault(): void; }; /** * Base class for event monitoring and dispatching. * * @template T - The type of events to monitor */ class Monitor { private registry = new Map>(); /** * Adds an event listener for the specified event type. * * @param name - The name of the event to listen for * @param handler - The function to call when the event occurs * @returns A function to remove the event listener */ public addEventListener(name: U, handler: T[U]) { const {registry} = this; const listeners = new Set(registry.get(name)); listeners.add(handler); registry.set(name, listeners); return () => this.removeEventListener(name, handler); } /** * Removes an event listener for the specified event type. * * @param name - The name of the event * @param handler - The function to remove */ public removeEventListener(name: keyof T, handler: T[keyof T]) { const {registry} = this; const listeners = new Set(registry.get(name)); listeners.delete(handler); registry.set(name, listeners); } /** * Dispatches an event to all registered listeners. * * @param name - The name of the event to dispatch * @param args - Arguments to pass to the event handlers */ protected dispatch(name: U, ...args: any[]) { const {registry} = this; const listeners = registry.get(name); if (!listeners) { return; } for (const listener of listeners) { listener(...args); } } } /** * Map of drag and drop event objects, keyed by event name. * Follows the same pattern as the DOM's `WindowEventMap`. * * @template T - The type of draggable entities * @template U - The type of droppable entities * @template V - The type of drag and drop manager */ export type DragDropEventMap< T extends Draggable, U extends Droppable, V extends DragDropManager, > = { /** Event fired when collisions are detected */ collision: Preventable<{ collisions: Collisions; }>; /** Event fired before a drag operation starts */ beforedragstart: Preventable<{ operation: DragOperationSnapshot; nativeEvent?: Event; }>; /** Event fired when a drag operation starts */ dragstart: { cancelable: false; operation: DragOperationSnapshot; nativeEvent?: Event; }; /** Event fired when a drag operation moves */ dragmove: Preventable<{ operation: DragOperationSnapshot; to?: Coordinates; by?: Coordinates; nativeEvent?: Event; }>; /** Event fired when a drag operation hovers over a droppable */ dragover: Preventable<{ operation: DragOperationSnapshot; }>; /** Event fired when a drag operation ends */ dragend: { operation: DragOperationSnapshot; nativeEvent?: Event; canceled: boolean; suspend(): {resume(): void; abort(): void}; }; }; /** * Map of drag and drop event handler signatures, keyed by event name. * Each handler receives the event object and the manager instance. * Derived from `DragDropEventMap`. * * @template T - The type of draggable entities * @template U - The type of droppable entities * @template V - The type of drag and drop manager */ export type DragDropEventHandlers< T extends Draggable, U extends Droppable, V extends DragDropManager, > = { [K in keyof DragDropEventMap]: ( event: DragDropEventMap[K], manager: V ) => void; }; export type CollisionEvent< T extends Draggable = Draggable, U extends Droppable = Droppable, V extends DragDropManager = DragDropManager, > = DragDropEventMap['collision']; export type BeforeDragStartEvent< T extends Draggable = Draggable, U extends Droppable = Droppable, V extends DragDropManager = DragDropManager, > = DragDropEventMap['beforedragstart']; export type DragStartEvent< T extends Draggable = Draggable, U extends Droppable = Droppable, V extends DragDropManager = DragDropManager, > = DragDropEventMap['dragstart']; export type DragMoveEvent< T extends Draggable = Draggable, U extends Droppable = Droppable, V extends DragDropManager = DragDropManager, > = DragDropEventMap['dragmove']; export type DragOverEvent< T extends Draggable = Draggable, U extends Droppable = Droppable, V extends DragDropManager = DragDropManager, > = DragDropEventMap['dragover']; export type DragEndEvent< T extends Draggable = Draggable, U extends Droppable = Droppable, V extends DragDropManager = DragDropManager, > = DragDropEventMap['dragend']; /** * Monitors and dispatches drag and drop events. * * @template T - The type of draggable entities * @template U - The type of droppable entities * @template V - The type of drag and drop manager */ export class DragDropMonitor< T extends Draggable, U extends Droppable, V extends DragDropManager, > extends Monitor> { /** * Creates a new drag and drop monitor. * * @param manager - The drag and drop manager to monitor */ constructor(private manager: V) { super(); } /** * Dispatches a drag and drop event. * * @param type - The type of event to dispatch * @param event - The event data to dispatch */ public dispatch>( type: Key, event: DragDropEventMap[Key] ) { const args = [event, this.manager] as any; super.dispatch(type, ...args); } } /** * Creates a preventable event object. * * @param event - The base event object * @param cancelable - Whether the event can be canceled * @returns A preventable event object */ export function defaultPreventable( event: T, cancelable = true ): Preventable { let defaultPrevented = false; return { ...event, cancelable, get defaultPrevented() { return defaultPrevented; }, preventDefault() { if (!cancelable) { return; } defaultPrevented = true; }, }; } ================================================ FILE: packages/abstract/src/core/manager/index.ts ================================================ export {DragDropManager, resolveCustomizable} from './manager.ts'; export type {DragDropManagerInput, Customizable} from './manager.ts'; export type {DragActions} from './actions.ts'; export type { DragDropEventMap, DragDropEventHandlers, CollisionEvent, BeforeDragStartEvent, DragStartEvent, DragMoveEvent, DragOverEvent, DragEndEvent, } from './events.ts'; export {Status as DragOperationStatus} from './status.ts'; export type {DragOperationSnapshot as DragOperation} from './operation.ts'; export type {DragDropRegistry} from './registry.ts'; export type {InferDraggable, InferDroppable} from './types.ts'; export type {Renderer} from './renderer.ts'; ================================================ FILE: packages/abstract/src/core/manager/manager.ts ================================================ import {effects, untracked} from '@dnd-kit/state'; import type {Draggable, Droppable} from '../entities/index.ts'; import {CollisionObserver, CollisionNotifier} from '../collision/index.ts'; import type {Plugins, Plugin} from '../plugins/index.ts'; import type {Sensor, Sensors} from '../sensors/index.ts'; import type {Modifier, Modifiers} from '../modifiers/index.ts'; import {descriptor} from '../plugins/utilities.ts'; import {DragActions} from './actions.ts'; import {DragDropRegistry} from './registry.ts'; import {DragOperation} from './operation.ts'; import {DragDropMonitor} from './events.ts'; import {defaultRenderer, type Renderer} from './renderer.ts'; /** * A value that can be provided as-is or as a function that receives defaults. * * @example * // As a plain array (replaces defaults) * plugins: [MyPlugin] * * // As a function (receives defaults) * plugins: (defaults) => [...defaults, MyPlugin] */ export type Customizable = T | ((defaults: T) => T); /** * Resolves a customizable value by applying it to defaults. * * @param value - A value or function that receives defaults * @param defaults - The default value to use when undefined, or to pass to a function * @returns The resolved value */ export function resolveCustomizable( value: Customizable | undefined, defaults: T ): T { if (typeof value === 'function') { return (value as (defaults: T) => T)(defaults); } return value ?? defaults; } export type DragDropManagerInput> = { plugins?: Customizable>; sensors?: Customizable>; modifiers?: Customizable>; renderer?: Renderer; }; /** * Central manager class that orchestrates drag and drop operations. * * @template T - The type of draggable entities * @template U - The type of droppable entities */ export class DragDropManager { /** Actions that can be performed during drag operations */ public actions: DragActions>; /** Observes and manages collision detection between draggable and droppable entities */ public collisionObserver: CollisionObserver; /** Tracks the current drag operation state and metadata */ public dragOperation: DragOperation; /** Monitors and emits drag and drop events */ public monitor: DragDropMonitor>; /** Registry that manages draggable and droppable entities */ public registry: DragDropRegistry>; /** Handles rendering of drag and drop visual feedback */ public renderer: Renderer; /** * Creates a new drag and drop manager instance. * * @param config - Optional configuration for plugins, sensors, modifiers, and renderer */ constructor(config?: DragDropManagerInput) { type V = DragDropManager; const raw = config ?? {}; const plugins = resolveCustomizable(raw.plugins, []); const sensors = resolveCustomizable(raw.sensors, []); const modifiers = resolveCustomizable(raw.modifiers, []); const renderer = raw.renderer ?? defaultRenderer; const monitor = new DragDropMonitor(this); const registry = new DragDropRegistry(this); this.registry = registry; this.monitor = monitor; this.renderer = renderer; this.actions = new DragActions(this); this.dragOperation = new DragOperation(this); this.collisionObserver = new CollisionObserver(this); this.plugins = [CollisionNotifier, ...plugins]; this.modifiers = modifiers; this.sensors = sensors; const {destroy} = this; const cleanup = effects(() => { const currentModifiers = untracked(() => this.dragOperation.modifiers); const managerModifiers = this.modifiers; for (const modifier of currentModifiers) { if (!managerModifiers.includes(modifier)) { modifier.destroy(); } } this.dragOperation.modifiers = this.dragOperation.source?.modifiers?.map((modifier) => { const {plugin, options} = descriptor(modifier); return new plugin(this, options); }) ?? managerModifiers; }); this.destroy = () => { cleanup(); destroy(); }; } /** * Gets the list of active plugins. * * @returns Array of active plugin instances */ get plugins(): Plugin[] { return this.registry.plugins.values; } /** * Sets the list of plugins to be used by the manager. * * @param plugins - Array of plugin constructors or instances */ set plugins(plugins: Plugins) { this.registry.plugins.values = plugins; } /** * Gets the list of active modifiers. * * @returns Array of active modifier instances */ get modifiers(): Modifier[] { return this.registry.modifiers.values; } /** * Sets the list of modifiers to be used by the manager. * * @param modifiers - Array of modifier constructors or instances */ set modifiers(modifiers: Modifiers) { this.registry.modifiers.values = modifiers; } /** * Gets the list of active sensors. * * @returns Array of active sensor instances */ get sensors(): Sensor[] { return this.registry.sensors.values; } /** * Sets the list of sensors to be used by the manager. * * @param sensors - Array of sensor constructors or instances */ set sensors(sensors: Sensors) { this.registry.sensors.values = sensors; } /** * Cleans up resources and stops any active drag operations. */ public destroy = () => { if (!this.dragOperation.status.idle) { this.actions.stop({canceled: true}); } this.dragOperation.modifiers.forEach((modifier) => modifier.destroy()); this.registry.destroy(); this.collisionObserver.destroy(); }; } ================================================ FILE: packages/abstract/src/core/manager/operation.ts ================================================ import {Position, type Shape} from '@dnd-kit/geometry'; import type {Coordinates} from '@dnd-kit/geometry'; import { batch, derived, reactive, snapshot, untracked, ValueHistory, type WithHistory, } from '@dnd-kit/state'; import type { Draggable, Droppable, UniqueIdentifier, } from '../entities/index.ts'; import type {Modifier} from '../modifiers/index.ts'; import type {DragDropManager} from './manager.ts'; import {Status, StatusValue} from './status.ts'; export interface DragOperationSnapshot< T extends Draggable = Draggable, U extends Droppable = Droppable, > { readonly activatorEvent: Event | null; readonly canceled: boolean; readonly position: Position; readonly transform: Coordinates; readonly status: Status; get shape(): WithHistory | null; set shape(value: Shape | null); readonly source: T | null; readonly target: U | null; } /** * Represents the current state of a drag operation. * * @template T - The type of draggable entities * @template U - The type of droppable entities */ export class DragOperation implements DragOperationSnapshot { /** * Creates a new drag operation instance. * * @param manager - The drag and drop manager that owns this operation */ constructor(manager: DragDropManager) { this.#manager = manager; } #manager: DragDropManager; #previousSource?: T; #shape = new ValueHistory(undefined, (a, b) => a && b ? a.equals(b) : a === b ); /** Current status of the drag operation */ public readonly status = new Status(); /** The controller for the currentdrag operation */ public controller: AbortController | undefined; /** * Gets the current shape of the dragged entity with history. * * @returns The shape history or null if no shape is set */ @derived public get shape(): WithHistory | null { const {current, initial, previous} = this.#shape; if (!current || !initial) { return null; } return {current, initial, previous}; } /** * Sets the shape of the dragged entity. * * @param value - The new shape or null to reset */ public set shape(value: Shape | null) { if (!value) { this.#shape.reset(); } else { this.#shape.current = value; } } /** Whether the drag operation was canceled */ @reactive public accessor canceled = false; /** The event that initiated the drag operation */ @reactive public accessor activatorEvent: Event | null = null; /** Unique identifier of the source draggable entity */ @reactive public accessor sourceIdentifier: UniqueIdentifier | null = null; /** Unique identifier of the target droppable entity */ @reactive public accessor targetIdentifier: UniqueIdentifier | null = null; /** List of modifiers applied to the drag operation */ @reactive public accessor modifiers: Modifier[] = []; /** Current position of the dragged entity */ public position = new Position({x: 0, y: 0}); /** * Gets the source draggable entity. * * @returns The current draggable entity, falling back to the previous * instance to bridge the gap when React unmounts and remounts a sortable * during reparenting (e.g. moving an item between columns). */ @derived public get source(): T | null { const identifier = this.sourceIdentifier; if (identifier == null) return null; const value = this.#manager.registry.draggables.get(identifier); if (value) { this.#previousSource = value; } return value ?? this.#previousSource ?? null; } /** * Gets the target droppable entity. * * @returns The current droppable entity or null if not found */ @derived public get target(): U | null { const identifier = this.targetIdentifier; return identifier != null ? (this.#manager.registry.droppables.get(identifier) ?? null) : null; } #transform = {x: 0, y: 0}; /** * Gets the current transform after applying all modifiers. * * @returns The transformed coordinates */ @derived public get transform() { const {x, y} = this.position.delta; let transform = {x, y}; for (const modifier of this.modifiers) { transform = modifier.apply({ ...this.snapshot(), transform, }); } this.#transform = transform; return transform; } /** * Creates a snapshot of the current drag operation state. * * @returns An immutable snapshot of the current operation state */ public snapshot(): DragOperationSnapshot { return untracked(() => ({ source: this.source, target: this.target, activatorEvent: this.activatorEvent, transform: this.#transform, shape: this.shape ? snapshot(this.shape) : null, position: snapshot(this.position), status: snapshot(this.status), canceled: this.canceled, })); } /** * Resets the drag operation to its initial state. * * @remarks * This method: * - Sets status to idle * - Clears source and target identifiers * - Resets shape history * - Resets position and transform * - Clears modifiers */ public reset() { batch(() => { this.status.set(StatusValue.Idle); this.sourceIdentifier = null; this.targetIdentifier = null; this.#shape.reset(); this.position.reset({x: 0, y: 0}); this.#transform = {x: 0, y: 0}; this.modifiers = []; }); } } ================================================ FILE: packages/abstract/src/core/manager/registry.ts ================================================ import type {CleanupFunction} from '@dnd-kit/state'; import { Draggable, Droppable, Entity, EntityRegistry, } from '../entities/index.ts'; import { PluginRegistry, Plugin, type PluginConstructor, PluginOptions, } from '../plugins/index.ts'; import { Sensor, SensorOptions, type SensorConstructor, } from '../sensors/index.ts'; import {Modifier, type ModifierConstructor} from '../modifiers/index.ts'; import type {DragDropManager} from './manager.ts'; /** * Manages the registration and lifecycle of draggable and droppable entities, * as well as plugins, sensors, and modifiers. * * @template T - The type of draggable entities * @template U - The type of droppable entities * @template V - The type of drag and drop manager */ export class DragDropRegistry< T extends Draggable, U extends Droppable, V extends DragDropManager, > { /** * Creates a new registry instance. * * @param manager - The drag and drop manager that owns this registry */ constructor(manager: V) { this.plugins = new PluginRegistry>(manager); this.sensors = new PluginRegistry>(manager); this.modifiers = new PluginRegistry>(manager); } /** Registry for draggable entities */ public draggables = new EntityRegistry(); /** Registry for droppable entities */ public droppables = new EntityRegistry(); /** Registry for plugins */ public plugins: PluginRegistry>; /** Registry for sensors */ public sensors: PluginRegistry>; /** Registry for modifiers */ public modifiers: PluginRegistry>; /** * Registers a new entity, plugin, sensor, or modifier. * * @param input - The entity, plugin constructor, sensor constructor, or modifier constructor to register * @param options - Optional configuration for plugins and sensors * @returns A cleanup function or the registered instance * @throws {Error} If the input type is invalid */ public register(input: Entity): () => void; public register(input: Draggable): () => void; public register(input: Droppable): () => void; public register(input: SensorConstructor, options?: SensorOptions): Sensor; public register(input: ModifierConstructor): Modifier; public register(input: PluginConstructor, options?: PluginOptions): Plugin; public register(input: any, options?: Record) { if (input instanceof Draggable) { return this.draggables.register(input.id, input as T); } if (input instanceof Droppable) { return this.droppables.register(input.id, input as U); } if (input.prototype instanceof Modifier) { return this.modifiers.register(input, options); } if (input.prototype instanceof Sensor) { return this.sensors.register(input, options); } if (input.prototype instanceof Plugin) { return this.plugins.register(input, options); } throw new Error('Invalid instance type'); } /** * Unregisters an entity, plugin, sensor, or modifier. * * @param input - The entity, plugin constructor, sensor constructor, or modifier constructor to unregister * @returns A cleanup function * @throws {Error} If the input type is invalid */ public unregister(input: Entity): CleanupFunction; public unregister(input: Draggable): CleanupFunction; public unregister(input: Droppable): CleanupFunction; public unregister(input: SensorConstructor): CleanupFunction; public unregister(input: ModifierConstructor): CleanupFunction; public unregister(input: PluginConstructor): CleanupFunction; public unregister(input: any) { if (input instanceof Entity) { if (input instanceof Draggable) { return this.draggables.unregister(input.id, input as T); } if (input instanceof Droppable) { return this.droppables.unregister(input.id, input as U); } // no-op return () => {}; } if (input.prototype instanceof Modifier) { return this.modifiers.unregister(input); } if (input.prototype instanceof Sensor) { return this.sensors.unregister(input); } if (input.prototype instanceof Plugin) { return this.plugins.unregister(input); } throw new Error('Invalid instance type'); } /** * Destroys all registered entities and cleans up resources. * * @remarks * This method: * - Destroys all draggable and droppable entities * - Destroys all plugins, sensors, and modifiers * - Cleans up any associated resources */ destroy() { this.draggables.destroy(); this.droppables.destroy(); this.plugins.destroy(); this.sensors.destroy(); this.modifiers.destroy(); } } ================================================ FILE: packages/abstract/src/core/manager/renderer.ts ================================================ /** * Interface for handling visual feedback during drag operations. * * @remarks * Implementations of this interface are responsible for managing * the visual state of dragged elements and ensuring smooth animations. */ export interface Renderer { /** * Gets a promise that resolves when the current rendering operation is complete. * * @returns A promise that resolves when rendering is finished */ get rendering(): Promise; } /** * Default renderer implementation. * * @remarks * This implementation immediately resolves rendering promises, * making it suitable for environments where custom rendering * is not required or handled externally. */ export const defaultRenderer: Renderer = { get rendering() { return Promise.resolve(); }, }; ================================================ FILE: packages/abstract/src/core/manager/status.ts ================================================ import {derived, reactive} from '@dnd-kit/state'; /** * Enum representing the possible states of a drag operation. */ export enum StatusValue { /** No drag operation is in progress */ Idle = 'idle', /** A drag operation is about to start */ InitializationPending = 'initialization-pending', /** A drag operation is being initialized */ Initializing = 'initializing', /** A drag operation is in progress */ Dragging = 'dragging', /** A drag operation has completed */ Dropped = 'dropped', } /** * Manages the status of a drag operation. * * @remarks * This class provides reactive accessors for checking the current state * of a drag operation and methods for updating it. */ export class Status { /** The current status value */ @reactive private accessor value: StatusValue = StatusValue.Idle; /** * Gets the current status value. * * @returns The current status value */ @derived public get current(): StatusValue { return this.value; } /** * Checks if the status is idle. * * @returns true if no drag operation is in progress */ @derived public get idle(): boolean { return this.value === StatusValue.Idle; } /** * Checks if the status is initializing. * * @returns true if a drag operation is being initialized */ @derived public get initializing(): boolean { return this.value === StatusValue.Initializing; } /** * Checks if the status is initialized. * * @returns true if a drag operation has started initialization */ @derived public get initialized(): boolean { const {value} = this; return ( value !== StatusValue.Idle && value !== StatusValue.InitializationPending ); } /** * Checks if the status is dragging. * * @returns true if a drag operation is in progress */ @derived public get dragging(): boolean { return this.value === StatusValue.Dragging; } /** * Checks if the status is dropped. * * @returns true if a drag operation has completed */ @derived public get dropped(): boolean { return this.value === StatusValue.Dropped; } /** * Sets the current status value. * * @param value - The new status value */ public set(value: StatusValue) { this.value = value; } } ================================================ FILE: packages/abstract/src/core/manager/types.ts ================================================ import type {DragDropManager} from './manager.ts'; /** * Infers the draggable type from a drag and drop manager type. * * @template P - The drag and drop manager type * @returns The inferred draggable type * * @example * ```typescript * type MyDraggable = InferDraggable>; * // MyDraggable is HTMLDivElement * ``` */ export type InferDraggable

    = P extends DragDropManager ? T : never; /** * Infers the droppable type from a drag and drop manager type. * * @template P - The drag and drop manager type * @returns The inferred droppable type * * @example * ```typescript * type MyDroppable = InferDroppable>; * // MyDroppable is HTMLDivElement * ``` */ export type InferDroppable

    = P extends DragDropManager ? T : never; ================================================ FILE: packages/abstract/src/core/modifiers/index.ts ================================================ export {Modifier} from './modifier.ts'; export type { Modifiers, ModifierConstructor, ModifierDescriptor, ModifierOptions, } from './modifier.ts'; ================================================ FILE: packages/abstract/src/core/modifiers/modifier.ts ================================================ import type {Coordinates} from '@dnd-kit/geometry'; import { Plugin, type PluginOptions, type PluginConstructor, type PluginDescriptor, } from '../plugins/index.ts'; import type {DragDropManager} from '../manager/manager.ts'; import type {DragOperationSnapshot} from '../manager/operation.ts'; /** Options that can be passed to a modifier */ export type ModifierOptions = PluginOptions; /** * Base class for drag operation modifiers. * * @template T - The type of drag and drop manager * @template U - The type of modifier options * * @remarks * Modifiers can transform the coordinates of a drag operation, * enabling features like snapping, constraints, and custom behaviors. */ export class Modifier< T extends DragDropManager = DragDropManager, U extends ModifierOptions = ModifierOptions, > extends Plugin { /** * Creates a new modifier instance. * * @param manager - The drag and drop manager that owns this modifier * @param options - Optional configuration for the modifier */ constructor( public manager: T, public options?: U ) { super(manager, options); } /** * Applies the modifier to the current drag operation. * * @param operation - The current state of the drag operation * @returns The transformed coordinates * * @remarks * Override this method to implement custom transformation logic. * The default implementation returns the original transform unchanged. */ public apply(operation: DragOperationSnapshot): Coordinates { return operation.transform; } } /** * Constructor type for modifiers. * * @template T - The type of drag and drop manager */ export type ModifierConstructor< T extends DragDropManager = DragDropManager, > = PluginConstructor>; /** * Descriptor type for modifiers. * * @template T - The type of drag and drop manager */ export type ModifierDescriptor< T extends DragDropManager = DragDropManager, > = PluginDescriptor, ModifierConstructor>; /** * Array type for modifier constructors or descriptors. * * @template T - The type of drag and drop manager */ export type Modifiers< T extends DragDropManager = DragDropManager, > = (ModifierConstructor | ModifierDescriptor)[]; ================================================ FILE: packages/abstract/src/core/plugins/index.ts ================================================ export {Plugin, CorePlugin} from './plugin.ts'; export {PluginRegistry} from './registry.ts'; export type { Plugins, PluginConstructor, PluginDescriptor, PluginOptions, } from './types.ts'; export {configure, configurator, descriptor} from './utilities.ts'; ================================================ FILE: packages/abstract/src/core/plugins/plugin.ts ================================================ import { type CleanupFunction, effect, reactive, untracked, } from '@dnd-kit/state'; import type {DragDropManager} from '../manager/index.ts'; import type {PluginOptions} from './types.ts'; import {configure} from './utilities.ts'; /** * Base class for plugins that extend drag and drop functionality. * * @template T - The type of drag and drop manager * @template U - The type of plugin options * * @remarks * Plugins can add new features and behaviors to the drag and drop system * by extending this class and implementing custom functionality. */ export abstract class Plugin< T extends DragDropManager = DragDropManager, U extends PluginOptions = PluginOptions, > { /** * Creates a new plugin instance. * * @param manager - The drag and drop manager that owns this plugin * @param options - Optional configuration for the plugin */ constructor( public manager: T, public options?: U ) {} /** * Whether the plugin instance is disabled. * * @remarks * This property is reactive and triggers effects when accessed. */ @reactive public accessor disabled: boolean = false; /** * Enables a disabled plugin instance. * * @remarks * This method triggers effects when called. */ public enable() { this.disabled = false; } /** * Disables an enabled plugin instance. * * @remarks * This method triggers effects when called. */ public disable() { this.disabled = true; } /** * Checks if the plugin instance is disabled. * * @returns true if the plugin is disabled * @remarks * This method does not trigger effects when accessed. */ public isDisabled() { return untracked(() => { return this.disabled; }); } /** * Configures a plugin instance with new options. * * @param options - The new options to apply */ public configure(options?: U) { this.options = options; } #cleanupFunctions = new Set(); /** * Registers an effect that will be cleaned up when the plugin is destroyed. * * @param callback - The effect callback to register * @returns A function to dispose of the effect */ protected registerEffect(callback: () => void) { const dispose = effect(callback.bind(this)); this.#cleanupFunctions.add(dispose); return dispose; } /** * Destroys a plugin instance and cleans up its resources. * * @remarks * This method: * - Calls all registered cleanup functions * - Should be overridden by subclasses to clean up additional resources */ public destroy() { this.#cleanupFunctions.forEach((cleanup) => cleanup()); } /** * Configures a plugin constructor with options. * * @param options - The options to configure the constructor with * @returns The configured plugin constructor * * @remarks * This method is used to configure the options that the * plugin constructor will use to create plugin instances. */ static configure(options: PluginOptions) { return configure(this as any, options); } } /** * Base class for core plugins that ship with the library. * * @template T - The type of drag and drop manager * @template U - The type of plugin options */ export class CorePlugin< T extends DragDropManager = DragDropManager, U extends PluginOptions = PluginOptions, > extends Plugin {} ================================================ FILE: packages/abstract/src/core/plugins/registry.ts ================================================ import {DragDropManager} from '../manager/index.ts'; import {CorePlugin, type Plugin} from './plugin.ts'; import type { InferPluginOptions, PluginDescriptor, PluginConstructor, Plugins, } from './types.ts'; import {descriptor} from './utilities.ts'; /** * Manages the registration and lifecycle of plugin instances. * * @template T - The type of drag and drop manager * @template W - The type of plugin constructor * @template U - The type of plugin instance */ export class PluginRegistry< T extends DragDropManager, W extends PluginConstructor = PluginConstructor, U extends Plugin = InstanceType, > { private instances: Map = new Map(); /** * Creates a new plugin registry. * * @param manager - The drag and drop manager that owns this registry */ constructor(private manager: T) {} /** * Gets all registered plugin instances. * * @returns An array of all active plugin instances */ public get values(): U[] { return Array.from(this.instances.values()); } #previousValues: PluginConstructor[] = []; /** * Sets the list of plugins to be used by the registry. * * @param entries - Array of plugin constructors or descriptors * @remarks * This method: * - Filters out duplicate plugins * - Unregisters plugins that are no longer in use * - Registers new plugins with their options */ public set values(entries: Plugins) { const descriptors = entries .map(descriptor) .reduce[]>((acc, descriptor) => { const existing = acc.find(({plugin}) => plugin === descriptor.plugin); if (existing) { // Keep the first occurrence's position, apply latest options existing.options = descriptor.options; return acc; } return [...acc, descriptor]; }, []); const constructors = descriptors.map(({plugin}) => plugin); for (const plugin of this.#previousValues) { if (!constructors.includes(plugin)) { if (plugin.prototype instanceof CorePlugin) { continue; } this.unregister(plugin as W); } } for (const {plugin, options} of descriptors) { this.register(plugin as W, options as InferPluginOptions); } this.#previousValues = constructors; } /** * Gets a plugin instance by its constructor. * * @param plugin - The plugin constructor to look up * @returns The plugin instance or undefined if not found */ public get(plugin: X): InstanceType | undefined { const instance = this.instances.get(plugin); return instance as any; } /** * Registers a new plugin instance. * * @param plugin - The plugin constructor to register * @param options - Optional configuration for the plugin * @returns The registered plugin instance * @remarks * If the plugin is already registered, its options will be updated * and the existing instance will be returned. */ public register( plugin: X, options?: InferPluginOptions ): InstanceType { const existingInstance = this.instances.get(plugin); if (existingInstance) { if (existingInstance.options !== options) { existingInstance.options = options; } return existingInstance as InstanceType; } const instance = new plugin(this.manager, options) as U; this.instances.set(plugin, instance); return instance as InstanceType; } /** * Unregisters a plugin instance. * * @param plugin - The plugin constructor to unregister * @remarks * This method: * - Destroys the plugin instance * - Removes it from the registry */ public unregister(plugin: X) { const instance = this.instances.get(plugin); if (instance) { instance.destroy(); this.instances.delete(plugin); } } /** * Destroys all registered plugin instances. * * @remarks * This method: * - Calls destroy() on all plugin instances * - Clears the registry */ public destroy() { for (const plugin of this.instances.values()) { plugin.destroy(); } this.instances.clear(); } } ================================================ FILE: packages/abstract/src/core/plugins/types.ts ================================================ import type {DragDropManager} from '../manager/index.ts'; import type {Plugin} from './plugin.ts'; /** Base type for plugin options */ export type PluginOptions = Record; /** * Constructor type for plugins. * * @template T - The type of drag and drop manager * @template U - The type of plugin instance * @template V - The type of plugin options */ export interface PluginConstructor< T extends DragDropManager = DragDropManager, U extends Plugin = Plugin, V extends PluginOptions = InferPluginOptions, > { /** Creates a new plugin instance */ new (manager: T, options?: V): U; } /** * Descriptor type for plugins. * * @template T - The type of drag and drop manager * @template U - The type of plugin instance * @template V - The type of plugin constructor */ export type PluginDescriptor< T extends DragDropManager = DragDropManager, U extends Plugin = Plugin, V extends PluginConstructor = PluginConstructor, > = { /** The plugin constructor */ plugin: V; /** Optional configuration for the plugin */ options?: InferPluginOptions; }; /** * Array type for plugin constructors or descriptors. * * @template T - The type of drag and drop manager */ export type Plugins< T extends DragDropManager = DragDropManager, > = (PluginConstructor | PluginDescriptor)[]; /** * Infers the options type from a plugin constructor or instance. * * @template P - The plugin constructor or instance type */ export type InferPluginOptions

    = P extends PluginConstructor ? T : P extends Plugin ? T : never; /** * Infers the manager type from a plugin instance. * * @template P - The plugin instance type */ export type InferManager

    = P extends Plugin> ? T : never; ================================================ FILE: packages/abstract/src/core/plugins/utilities.ts ================================================ import type { PluginConstructor, PluginOptions, PluginDescriptor, InferPluginOptions, } from './types.ts'; /** * Creates a plugin descriptor with the given plugin constructor and options. * * @template T - The plugin constructor type * @template V - The plugin options type * @param plugin - The plugin constructor * @param options - The plugin configuration options * @returns A plugin descriptor containing the constructor and options */ export function configure< T extends PluginConstructor, V extends PluginOptions = InferPluginOptions, >(plugin: T, options: V): PluginDescriptor { return { plugin, options, }; } /** * Creates a configurator function for a specific plugin constructor. * * @template T - The plugin constructor type * @param plugin - The plugin constructor to configure * @returns A function that takes options and returns a plugin descriptor */ export function configurator>( plugin: T ) { return (options: InferPluginOptions): PluginDescriptor => { return configure(plugin, options); }; } /** * Normalizes a plugin constructor or descriptor into a descriptor. * * @template T - The plugin constructor type * @param plugin - Either a plugin constructor or a plugin descriptor * @returns A plugin descriptor */ export function descriptor>( plugin: T | PluginDescriptor ): PluginDescriptor { if (typeof plugin === 'function') { return { plugin, options: undefined, }; } return plugin; } ================================================ FILE: packages/abstract/src/core/sensors/activation.ts ================================================ export class ActivationController extends AbortController { public activated = false; constructor( private constraints: ActivationConstraints | undefined, private onActivate: (event: E) => void ) { super(); for (const constraint of constraints ?? []) { constraint.controller = this; } } onEvent(event: E) { if (this.activated) return; if (this.constraints?.length) { for (const constraint of this.constraints) { constraint.onEvent(event); } } else { this.activate(event); } } activate(event: E) { if (this.activated) return; this.activated = true; this.onActivate(event); } abort(event?: E) { this.activated = false; super.abort(event); } } export interface ActivationConstraintOptions {} export abstract class ActivationConstraint< E extends Event = Event, O extends ActivationConstraintOptions = ActivationConstraintOptions, > { #controller: ActivationController | undefined; set controller(controller: ActivationController) { this.#controller = controller; controller.signal.addEventListener('abort', () => this.abort()); } constructor(protected options: O) {} /** * Called when the activation is triggered. */ public activate(event: E): void { this.#controller?.activate(event); } /** * Called when the activation is aborted. */ public abstract abort(event?: E): void; /** * Called when an input event is received by the sensor. * Returns `true` if this event triggers activation immediately. */ public abstract onEvent(event: E): void; } export type ActivationConstraints = ActivationConstraint[]; ================================================ FILE: packages/abstract/src/core/sensors/index.ts ================================================ export {Sensor} from './sensor.ts'; export type { Sensors, SensorConstructor, SensorDescriptor, SensorOptions, } from './sensor.ts'; export { ActivationConstraint, type ActivationConstraints, } from './activation.ts'; export {ActivationController} from './activation.ts'; ================================================ FILE: packages/abstract/src/core/sensors/sensor.ts ================================================ import {CleanupFunction} from '@dnd-kit/state'; import type {DragDropManager} from '../manager/index.ts'; import type {Draggable, Droppable} from '../entities/index.ts'; import { Plugin, type PluginConstructor, type PluginDescriptor, type PluginOptions, } from '../plugins/index.ts'; /** * Options that can be passed to a sensor. * Extends the base PluginOptions type. */ export type SensorOptions = PluginOptions; /** * Abstract base class for all sensor implementations. * * @template T - The type of drag drop manager * @template U - The type of sensor options * * @remarks * Sensors are responsible for detecting and initiating drag operations. * They handle the actual user interaction (mouse, touch, keyboard, etc.) * and translate those interactions into drag operations. */ export abstract class Sensor< T extends DragDropManager = DragDropManager, U extends SensorOptions = SensorOptions, > extends Plugin { /** * Creates a new sensor instance. * * @param manager - The drag drop manager instance * @param options - Optional sensor configuration */ constructor( public manager: T, public options?: U ) { super(manager, options); } /** * Binds the sensor to a draggable source. * * @param source - The draggable element to bind to * @param options - Optional sensor options specific to this draggable * @returns A cleanup function to unbind the sensor */ public abstract bind(source: Draggable, options?: U): CleanupFunction; } /** * Constructor type for creating sensor instances. * * @template T - The type of drag drop manager */ export type SensorConstructor< T extends DragDropManager = DragDropManager, > = PluginConstructor>; /** * Descriptor type for configuring sensors. * * @template T - The type of drag drop manager */ export type SensorDescriptor< T extends DragDropManager = DragDropManager, > = PluginDescriptor, SensorConstructor>; /** * Array type for multiple sensor configurations. * * @template T - The type of drag drop manager */ export type Sensors< T extends DragDropManager = DragDropManager, > = (SensorConstructor | SensorDescriptor)[]; ================================================ FILE: packages/abstract/src/modifiers/axis.ts ================================================ import { configurator, Modifier, type DragDropManager, type DragOperation, } from '@dnd-kit/abstract'; /** * Options for configuring an axis modifier. * * @property axis - The axis to restrict movement to ('x' or 'y') * @property value - The fixed value to set for the specified axis */ interface Options { axis: 'x' | 'y'; value: number; } /** * A modifier that restricts drag movement to a specific axis and value. * * @remarks * This modifier can be used to: * - Restrict movement to a specific axis * - Set a fixed value for the specified axis * - Create horizontal or vertical movement constraints */ export class AxisModifier extends Modifier, Options> { /** * Applies the axis restriction to the drag operation. * * @param operation - The current drag operation * @returns The modified transform with the axis restriction applied */ apply({transform}: DragOperation) { if (!this.options) { return transform; } const {axis, value} = this.options; return { ...transform, [axis]: value, }; } /** * Creates a configured instance of the AxisModifier. * * @param options - The axis restriction options * @returns A configured AxisModifier instance */ static configure = configurator(AxisModifier); } /** * A pre-configured modifier that restricts movement to the vertical axis. * * @remarks * This modifier fixes the x-axis value to 0, allowing only vertical movement. */ export const RestrictToVerticalAxis = AxisModifier.configure({ axis: 'x', value: 0, }); /** * A pre-configured modifier that restricts movement to the horizontal axis. * * @remarks * This modifier fixes the y-axis value to 0, allowing only horizontal movement. */ export const RestrictToHorizontalAxis = AxisModifier.configure({ axis: 'y', value: 0, }); ================================================ FILE: packages/abstract/src/modifiers/boundingRectangle.ts ================================================ import type {BoundingRectangle, Coordinates, Shape} from '@dnd-kit/geometry'; /** * Restricts a shape's movement to stay within a bounding rectangle. * * @param shape - The shape to restrict * @param transform - The current transform coordinates * @param boundingRect - The bounding rectangle to restrict movement within * @returns The modified transform coordinates that keep the shape within bounds * * @remarks * This function: * - Prevents the shape from moving outside the bounding rectangle * - Adjusts the transform coordinates to keep the shape's edges within bounds * - Maintains the shape's position relative to the bounding rectangle * * @example * ```typescript * const shape = { boundingRectangle: { top: 0, left: 0, right: 100, bottom: 100 } }; * const transform = { x: 50, y: 50 }; * const bounds = { top: 0, left: 0, width: 200, height: 200 }; * * const restricted = restrictShapeToBoundingRectangle(shape, transform, bounds); * ``` */ export function restrictShapeToBoundingRectangle( shape: Shape, transform: Coordinates, boundingRect: BoundingRectangle ) { const value = { ...transform, }; if (shape.boundingRectangle.top + transform.y <= boundingRect.top) { value.y = boundingRect.top - shape.boundingRectangle.top; } else if ( shape.boundingRectangle.bottom + transform.y >= boundingRect.top + boundingRect.height ) { value.y = boundingRect.top + boundingRect.height - shape.boundingRectangle.bottom; } if (shape.boundingRectangle.left + transform.x <= boundingRect.left) { value.x = boundingRect.left - shape.boundingRectangle.left; } else if ( shape.boundingRectangle.right + transform.x >= boundingRect.left + boundingRect.width ) { value.x = boundingRect.left + boundingRect.width - shape.boundingRectangle.right; } return value; } ================================================ FILE: packages/abstract/src/modifiers/index.ts ================================================ export { AxisModifier, RestrictToHorizontalAxis, RestrictToVerticalAxis, } from './axis.ts'; export {restrictShapeToBoundingRectangle} from './boundingRectangle.ts'; export {SnapModifier} from './snap.ts'; ================================================ FILE: packages/abstract/src/modifiers/snap.ts ================================================ import { configurator, Modifier, type DragDropManager, type DragOperation, } from '@dnd-kit/abstract'; /** * Options for configuring a snap modifier. * * @property size - The grid size to snap to, either a single number for both axes * or separate x and y values */ interface Options { size: number | {x: number; y: number}; } /** * A modifier that snaps drag movement to a grid. * * @remarks * This modifier: * - Snaps drag coordinates to the nearest grid point * - Supports different grid sizes for x and y axes * - Uses ceiling rounding to ensure consistent snapping behavior * * @example * ```typescript * // Snap to a 20x20 grid * const modifier = SnapModifier.configure({ size: 20 }); * * // Snap to a 10x20 grid * const modifier = SnapModifier.configure({ size: { x: 10, y: 20 } }); * ``` */ export class SnapModifier extends Modifier, Options> { /** * Applies the snap grid to the drag operation. * * @param operation - The current drag operation * @returns The modified transform with coordinates snapped to the grid */ apply({transform}: DragOperation) { const {size = 20} = this.options ?? {}; const x = typeof size === 'number' ? size : size.x; const y = typeof size === 'number' ? size : size.y; return { ...transform, x: Math.ceil(transform.x / x) * x, y: Math.ceil(transform.y / y) * y, }; } /** * Creates a configured instance of the SnapModifier. * * @param options - The snap grid options * @returns A configured SnapModifier instance */ static configure = configurator(SnapModifier); } ================================================ FILE: packages/abstract/tests/manager-modifiers.test.ts ================================================ import {describe, expect, it} from 'bun:test'; import {batch} from '@dnd-kit/state'; import { DragDropManager, Modifier, Draggable, configure, } from '@dnd-kit/abstract'; class ClampXModifier extends Modifier { public apply(operation: Parameters[0]) { return {x: 0, y: operation.transform.y}; } } class ClampYModifier extends Modifier { public apply(operation: Parameters[0]) { return {x: operation.transform.x, y: 0}; } } /** Flush microtasks so async stop/reset logic completes */ function flush() { return new Promise((resolve) => setTimeout(resolve, 10)); } describe('Manager-level modifiers', () => { it('should apply manager modifiers when draggable has none', () => { const manager = new DragDropManager({modifiers: [ClampXModifier]}); const draggable = new Draggable({id: 'd1', register: false}, manager); draggable.register(); manager.actions.start({source: draggable, coordinates: {x: 0, y: 0}}); batch(() => { manager.dragOperation.position.current = {x: 100, y: 50}; }); expect(manager.dragOperation.transform).toEqual({x: 0, y: 50}); draggable.destroy(); manager.destroy(); }); it('should apply configured manager modifiers', () => { const manager = new DragDropManager({ modifiers: [configure(ClampXModifier, undefined)], }); const draggable = new Draggable({id: 'd1', register: false}, manager); draggable.register(); manager.actions.start({source: draggable, coordinates: {x: 0, y: 0}}); batch(() => { manager.dragOperation.position.current = {x: 100, y: 50}; }); expect(manager.dragOperation.transform).toEqual({x: 0, y: 50}); draggable.destroy(); manager.destroy(); }); it('should prefer draggable modifiers over manager modifiers', () => { const manager = new DragDropManager({modifiers: [ClampXModifier]}); const draggable = new Draggable( {id: 'd1', modifiers: [ClampYModifier], register: false}, manager ); draggable.register(); manager.actions.start({source: draggable, coordinates: {x: 0, y: 0}}); batch(() => { manager.dragOperation.position.current = {x: 100, y: 50}; }); expect(manager.dragOperation.transform).toEqual({x: 100, y: 0}); draggable.destroy(); manager.destroy(); }); }); describe('Manager modifier lifecycle', () => { it('should not destroy manager modifiers when drag starts', () => { let destroyCount = 0; class TrackedModifier extends ClampXModifier { public destroy() { destroyCount++; super.destroy(); } } const manager = new DragDropManager({modifiers: [TrackedModifier]}); const draggable = new Draggable({id: 'd1', register: false}, manager); draggable.register(); manager.actions.start({source: draggable, coordinates: {x: 0, y: 0}}); expect(destroyCount).toBe(0); draggable.destroy(); manager.destroy(); }); it('should not destroy manager modifiers when drag stops', async () => { let destroyCount = 0; class TrackedModifier extends ClampXModifier { public destroy() { destroyCount++; super.destroy(); } } const manager = new DragDropManager({modifiers: [TrackedModifier]}); const draggable = new Draggable({id: 'd1', register: false}, manager); draggable.register(); manager.actions.start({source: draggable, coordinates: {x: 0, y: 0}}); manager.actions.stop(); await flush(); expect(destroyCount).toBe(0); draggable.destroy(); manager.destroy(); }); it('should keep manager modifiers working across multiple drags', async () => { const manager = new DragDropManager({modifiers: [ClampXModifier]}); const draggable = new Draggable({id: 'd1', register: false}, manager); draggable.register(); // First drag manager.actions.start({source: draggable, coordinates: {x: 0, y: 0}}); batch(() => { manager.dragOperation.position.current = {x: 100, y: 50}; }); expect(manager.dragOperation.transform).toEqual({x: 0, y: 50}); manager.actions.stop(); await flush(); // Second drag — manager modifiers must still be active manager.actions.start({source: draggable, coordinates: {x: 0, y: 0}}); batch(() => { manager.dragOperation.position.current = {x: 200, y: 75}; }); expect(manager.dragOperation.transform).toEqual({x: 0, y: 75}); draggable.destroy(); manager.destroy(); }); it('should destroy per-operation modifiers when draggable modifiers change mid-drag', () => { let destroyCount = 0; class TrackedModifier extends ClampXModifier { public destroy() { destroyCount++; super.destroy(); } } const manager = new DragDropManager({}); const draggable = new Draggable( {id: 'd1', modifiers: [TrackedModifier], register: false}, manager ); draggable.register(); manager.actions.start({source: draggable, coordinates: {x: 0, y: 0}}); expect(destroyCount).toBe(0); // Changing the draggable's modifiers mid-drag triggers the effect, // which should destroy the old per-operation instances draggable.modifiers = [ClampYModifier]; expect(destroyCount).toBe(1); draggable.destroy(); manager.destroy(); }); it('should destroy per-operation modifiers when switching to manager modifiers mid-drag', () => { let destroyCount = 0; class TrackedModifier extends ClampXModifier { public destroy() { destroyCount++; super.destroy(); } } const manager = new DragDropManager({modifiers: [ClampYModifier]}); const draggable = new Draggable( {id: 'd1', modifiers: [TrackedModifier], register: false}, manager ); draggable.register(); manager.actions.start({source: draggable, coordinates: {x: 0, y: 0}}); expect(destroyCount).toBe(0); // Removing draggable modifiers should fall back to manager modifiers // and destroy the per-operation instances draggable.modifiers = undefined; expect(destroyCount).toBe(1); // Manager modifiers should now be active batch(() => { manager.dragOperation.position.current = {x: 100, y: 50}; }); expect(manager.dragOperation.transform).toEqual({x: 100, y: 0}); draggable.destroy(); manager.destroy(); }); it('should not destroy manager modifiers when different draggables are used', async () => { let destroyCount = 0; class TrackedModifier extends ClampXModifier { public destroy() { destroyCount++; super.destroy(); } } const manager = new DragDropManager({modifiers: [TrackedModifier]}); const draggableA = new Draggable({id: 'a', register: false}, manager); const draggableB = new Draggable({id: 'b', register: false}, manager); draggableA.register(); draggableB.register(); // First drag with draggable A manager.actions.start({source: draggableA, coordinates: {x: 0, y: 0}}); manager.actions.stop(); await flush(); expect(destroyCount).toBe(0); // Second drag with draggable B manager.actions.start({source: draggableB, coordinates: {x: 0, y: 0}}); batch(() => { manager.dragOperation.position.current = {x: 100, y: 50}; }); expect(manager.dragOperation.transform).toEqual({x: 0, y: 50}); expect(destroyCount).toBe(0); draggableA.destroy(); draggableB.destroy(); manager.destroy(); }); }); ================================================ FILE: packages/abstract/tests/plugin-registry.test.ts ================================================ import {describe, expect, it} from 'bun:test'; import {batch} from '@dnd-kit/state'; import { DragDropManager, Draggable, Plugin, CorePlugin, configure, } from '@dnd-kit/abstract'; interface TestOptions { value?: string; } class PluginA extends Plugin { constructor(manager: DragDropManager, public options?: TestOptions) { super(manager, options); } } class PluginB extends Plugin { constructor(manager: DragDropManager, public options?: TestOptions) { super(manager, options); } } class PluginC extends CorePlugin { constructor(manager: DragDropManager, public options?: TestOptions) { super(manager, options); } } function indexOf(manager: DragDropManager, PluginClass: any): number { return manager.registry.plugins.values.findIndex( (p) => p.constructor === PluginClass ); } describe('PluginRegistry duplicate handling', () => { it('should keep the first occurrence when duplicates exist', () => { const manager = new DragDropManager({ plugins: [PluginA, PluginB, PluginA], }); const pluginA = manager.registry.plugins.get(PluginA); const pluginB = manager.registry.plugins.get(PluginB); expect(pluginA).toBeInstanceOf(PluginA); expect(pluginB).toBeInstanceOf(PluginB); expect(indexOf(manager, PluginA)).toBeLessThan(indexOf(manager, PluginB)); manager.destroy(); }); it('should apply options from the last occurrence to the first position', () => { const manager = new DragDropManager({ plugins: [PluginA, PluginB, configure(PluginA, {value: 'custom'})], }); const pluginA = manager.registry.plugins.get(PluginA); expect(pluginA).toBeDefined(); expect(pluginA!.options).toEqual({value: 'custom'}); expect(indexOf(manager, PluginA)).toBeLessThan(indexOf(manager, PluginB)); manager.destroy(); }); it('should preserve registration order so earlier plugins are available to later ones', () => { let resolvedDep: Plugin | undefined; class DepPlugin extends Plugin { constructor(manager: DragDropManager) { super(manager); } } class ConsumerPlugin extends Plugin { constructor(manager: DragDropManager) { super(manager); resolvedDep = manager.registry.plugins.get(DepPlugin); } } const manager = new DragDropManager({ plugins: [DepPlugin, ConsumerPlugin, configure(DepPlugin, {value: 'x'})], }); expect(resolvedDep).toBeInstanceOf(DepPlugin); manager.destroy(); }); it('should handle configure with empty options', () => { const manager = new DragDropManager({ plugins: [PluginA, PluginB, configure(PluginA, {})], }); const pluginA = manager.registry.plugins.get(PluginA); expect(pluginA).toBeDefined(); expect(pluginA!.options).toEqual({}); expect(indexOf(manager, PluginA)).toBeLessThan(indexOf(manager, PluginB)); manager.destroy(); }); it('should not create duplicate instances when the same plugin appears multiple times', () => { let instanceCount = 0; class TrackedPlugin extends Plugin { constructor(manager: DragDropManager) { super(manager); instanceCount++; } } const manager = new DragDropManager({ plugins: [TrackedPlugin, PluginB, configure(TrackedPlugin, {value: 'x'})], }); expect(instanceCount).toBe(1); manager.destroy(); }); it('should apply the rightmost options when a plugin appears more than twice', () => { const manager = new DragDropManager({ plugins: [ configure(PluginA, {value: 'first'}), PluginB, configure(PluginA, {value: 'second'}), configure(PluginA, {value: 'third'}), ], }); const pluginA = manager.registry.plugins.get(PluginA); expect(pluginA!.options).toEqual({value: 'third'}); expect(indexOf(manager, PluginA)).toBeLessThan(indexOf(manager, PluginB)); manager.destroy(); }); }); describe('Per-entity plugin auto-registration', () => { it('should auto-register plugins from entity plugins array', () => { const manager = new DragDropManager({}); const draggable = new Draggable( {id: 'd1', plugins: [PluginA], register: false}, manager ); draggable.register(); expect(manager.registry.plugins.get(PluginA)).toBeInstanceOf(PluginA); draggable.destroy(); manager.destroy(); }); it('should auto-register configured plugins without applying entity options globally', () => { const manager = new DragDropManager({}); const draggable = new Draggable( { id: 'd1', plugins: [configure(PluginA, {value: 'entity-only'})], register: false, }, manager ); draggable.register(); const globalInstance = manager.registry.plugins.get(PluginA); expect(globalInstance).toBeInstanceOf(PluginA); expect(globalInstance!.options).toBeUndefined(); draggable.destroy(); manager.destroy(); }); it('should not duplicate-register already-registered plugins', () => { let instanceCount = 0; class TrackedPlugin extends Plugin { constructor(manager: DragDropManager) { super(manager); instanceCount++; } } const manager = new DragDropManager({plugins: [TrackedPlugin]}); expect(instanceCount).toBe(1); const draggable = new Draggable( {id: 'd1', plugins: [TrackedPlugin], register: false}, manager ); draggable.register(); expect(instanceCount).toBe(1); draggable.destroy(); manager.destroy(); }); it('should keep auto-registered plugin alive after source entity is destroyed mid-drag', async () => { const manager = new DragDropManager({}); const draggable = new Draggable( {id: 'd1', plugins: [PluginA], register: false}, manager ); draggable.register(); manager.actions.start({source: draggable, coordinates: {x: 0, y: 0}}); expect(manager.registry.plugins.get(PluginA)).toBeInstanceOf(PluginA); draggable.destroy(); expect(manager.registry.plugins.get(PluginA)).toBeInstanceOf(PluginA); manager.actions.stop(); await new Promise((resolve) => setTimeout(resolve, 10)); manager.destroy(); }); }); describe('Per-entity plugin config (pluginConfig)', () => { it('should return per-entity options for a configured plugin', () => { const manager = new DragDropManager({}); const draggable = new Draggable( { id: 'd1', plugins: [configure(PluginA, {value: 'custom'})], register: false, }, manager ); expect(draggable.pluginConfig(PluginA)).toEqual({value: 'custom'}); draggable.destroy(); manager.destroy(); }); it('should return undefined for unconfigured plugins', () => { const manager = new DragDropManager({}); const draggable = new Draggable({id: 'd1', register: false}, manager); expect(draggable.pluginConfig(PluginA)).toBeUndefined(); draggable.destroy(); manager.destroy(); }); it('should return undefined when entity has no plugins even if plugin is registered globally', () => { const manager = new DragDropManager({plugins: [PluginA]}); const draggable = new Draggable({id: 'd1', register: false}, manager); expect(manager.registry.plugins.get(PluginA)).toBeInstanceOf(PluginA); expect(draggable.pluginConfig(PluginA)).toBeUndefined(); draggable.destroy(); manager.destroy(); }); it('should isolate per-entity config between different entities', () => { const manager = new DragDropManager({}); const draggableA = new Draggable( { id: 'a', plugins: [configure(PluginA, {value: 'alpha'})], register: false, }, manager ); const draggableB = new Draggable( { id: 'b', plugins: [configure(PluginA, {value: 'beta'})], register: false, }, manager ); draggableA.register(); draggableB.register(); expect(draggableA.pluginConfig(PluginA)).toEqual({value: 'alpha'}); expect(draggableB.pluginConfig(PluginA)).toEqual({value: 'beta'}); const globalInstance = manager.registry.plugins.get(PluginA); expect(globalInstance!.options).toBeUndefined(); draggableA.destroy(); draggableB.destroy(); manager.destroy(); }); it('should return per-entity options even when plugin is globally configured with different options', () => { const manager = new DragDropManager({ plugins: [configure(PluginA, {value: 'global'})], }); const draggable = new Draggable( { id: 'd1', plugins: [configure(PluginA, {value: 'entity'})], register: false, }, manager ); expect(draggable.pluginConfig(PluginA)).toEqual({value: 'entity'}); expect(manager.registry.plugins.get(PluginA)!.options).toEqual({ value: 'global', }); draggable.destroy(); manager.destroy(); }); it('should reflect updated options when plugins property is reassigned', () => { const manager = new DragDropManager({}); const draggable = new Draggable( { id: 'd1', plugins: [configure(PluginA, {value: 'first'})], register: false, }, manager ); expect(draggable.pluginConfig(PluginA)).toEqual({value: 'first'}); draggable.plugins = [configure(PluginA, {value: 'second'})]; expect(draggable.pluginConfig(PluginA)).toEqual({value: 'second'}); draggable.destroy(); manager.destroy(); }); }); ================================================ FILE: packages/abstract/tsconfig.json ================================================ { "extends": "../../config/typescript/vanilla.json", "include": ["src/**/*"], "exclude": ["dist", "build", "node_modules"] } ================================================ FILE: packages/abstract/tsup.config.ts ================================================ import {defineConfig} from 'tsup'; export default defineConfig((options) => ({ dts: true, outDir: './', external: ['@dnd-kit/abstract'], format: ['esm', 'cjs'], sourcemap: true, treeshake: !options.watch, })); ================================================ FILE: packages/collision/CHANGELOG.md ================================================ # @dnd-kit/collision ## 0.3.2 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.3.2 - @dnd-kit/geometry@0.3.2 ## 0.3.1 ### Patch Changes - Updated dependencies [[`4341114`](https://github.com/clauderic/dnd-kit/commit/43411143063349caeded4f778923473624ce25cf)]: - @dnd-kit/abstract@0.3.1 - @dnd-kit/geometry@0.3.1 ## 0.3.0 ### Patch Changes - Updated dependencies [[`6a59647`](https://github.com/clauderic/dnd-kit/commit/6a59647ebba2114b2e423f282ab25bf2ea40318d)]: - @dnd-kit/abstract@0.3.0 - @dnd-kit/geometry@0.3.0 ## 0.2.4 ### Patch Changes - [#1866](https://github.com/clauderic/dnd-kit/pull/1866) [`256432d`](https://github.com/clauderic/dnd-kit/commit/256432dec8823342765eebdb78ee791c25fea382) Thanks [@github-actions](https://github.com/apps/github-actions)! - **directionBiased**: Fix inverted logic to bias towards shapes above or below the drag operation shape. - Updated dependencies [[`de27fbc`](https://github.com/clauderic/dnd-kit/commit/de27fbca9df12eece3cd53ccbbac34e0eaf113e1), [`be7cfe3`](https://github.com/clauderic/dnd-kit/commit/be7cfe3b6cf6a989aefd3e39fd145fe271942b3a)]: - @dnd-kit/abstract@0.2.4 - @dnd-kit/geometry@0.2.4 ## 0.2.3 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.2.3 - @dnd-kit/geometry@0.2.3 ## 0.2.2 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.2.2 - @dnd-kit/geometry@0.2.2 ## 0.2.1 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.2.1 - @dnd-kit/geometry@0.2.1 ## 0.2.0 ### Patch Changes - Updated dependencies [[`e95a9c8`](https://github.com/clauderic/dnd-kit/commit/e95a9c8f448d6b339e0b6fd37546ac7cfdf18edb)]: - @dnd-kit/abstract@0.2.0 - @dnd-kit/geometry@0.2.0 ## 0.1.21 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.21 - @dnd-kit/geometry@0.1.21 ## 0.1.20 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.20 - @dnd-kit/geometry@0.1.20 ## 0.1.19 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.19 - @dnd-kit/geometry@0.1.19 ## 0.1.18 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.18 - @dnd-kit/geometry@0.1.18 ## 0.1.17 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.17 - @dnd-kit/geometry@0.1.17 ## 0.1.16 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.16 - @dnd-kit/geometry@0.1.16 ## 0.1.15 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.15 - @dnd-kit/geometry@0.1.15 ## 0.1.14 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.14 - @dnd-kit/geometry@0.1.14 ## 0.1.13 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.13 - @dnd-kit/geometry@0.1.13 ## 0.1.12 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.12 - @dnd-kit/geometry@0.1.12 ## 0.1.11 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.11 - @dnd-kit/geometry@0.1.11 ## 0.1.10 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.10 - @dnd-kit/geometry@0.1.10 ## 0.1.9 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.9 - @dnd-kit/geometry@0.1.9 ## 0.1.8 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.8 - @dnd-kit/geometry@0.1.8 ## 0.1.7 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.7 - @dnd-kit/geometry@0.1.7 ## 0.1.6 ### Patch Changes - Updated dependencies [[`7ceb799`](https://github.com/clauderic/dnd-kit/commit/7ceb799c7d214bc8223ec845357a0040c28ae40e)]: - @dnd-kit/abstract@0.1.6 - @dnd-kit/geometry@0.1.6 ## 0.1.5 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.5 - @dnd-kit/geometry@0.1.5 ## 0.1.4 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.1.4 - @dnd-kit/geometry@0.1.4 ## 0.1.3 ### Patch Changes - Updated dependencies [[`6c9a9ea`](https://github.com/clauderic/dnd-kit/commit/6c9a9ea060095884c90c72cd5d6b73820467ec29), [`1bef872`](https://github.com/clauderic/dnd-kit/commit/1bef8722d515079f998dc0608084e1d853e74d3a), [`2522836`](https://github.com/clauderic/dnd-kit/commit/2522836fdb80520913ea35d94c6558bf7784afc9)]: - @dnd-kit/abstract@0.1.3 - @dnd-kit/geometry@0.1.3 ## 0.1.2 ### Patch Changes - Updated dependencies [[`ee55f58`](https://github.com/clauderic/dnd-kit/commit/ee55f582f92dc42cc6eea9ad7492fc782ca6455a), [`4682570`](https://github.com/clauderic/dnd-kit/commit/4682570a6b80868af0e51b1bbbf902430117df43), [`f8d69b0`](https://github.com/clauderic/dnd-kit/commit/f8d69b01f4cf53fc368ef1fca9188c313192928d), [`d04e9a2`](https://github.com/clauderic/dnd-kit/commit/d04e9a2879fb00f092c3f8280c8081a48eebf193), [`ee55f58`](https://github.com/clauderic/dnd-kit/commit/ee55f582f92dc42cc6eea9ad7492fc782ca6455a)]: - @dnd-kit/geometry@0.1.2 - @dnd-kit/abstract@0.1.2 ## 0.1.1 ### Patch Changes - Updated dependencies [[`f13cbc9`](https://github.com/clauderic/dnd-kit/commit/f13cbc978229844770d3c8aa03135e4352ee2532)]: - @dnd-kit/abstract@0.1.1 - @dnd-kit/geometry@0.1.1 ## 0.1.0 ### Patch Changes - Updated dependencies [[`00a33c9`](https://github.com/clauderic/dnd-kit/commit/00a33c99e777ab205a45309a4efc8b3560bafdaf)]: - @dnd-kit/abstract@0.1.0 - @dnd-kit/geometry@0.1.0 ## 0.0.10 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.0.10 - @dnd-kit/geometry@0.0.10 ## 0.0.9 ### Patch Changes - [#1600](https://github.com/clauderic/dnd-kit/pull/1600) [`3463da1`](https://github.com/clauderic/dnd-kit/commit/3463da1cac9f26e4b2ab3278ae52206bb99645e4) Thanks [@github-actions](https://github.com/apps/github-actions)! - Changed the `closestCorners` algorithm to use the corners of the drag operation shape instead of the center of the shape. - [#1600](https://github.com/clauderic/dnd-kit/pull/1600) [`8ae7014`](https://github.com/clauderic/dnd-kit/commit/8ae70143bc404bff7678fa8e8390a640c16f2579) Thanks [@github-actions](https://github.com/apps/github-actions)! - - Added `corners` getter to `Rectangle` instances to retrieve the coordinates of all four corners. - Added `delta` static method to the `Rectangle` constructor which makes it easy to calculate the delta between a given reference point of both shapes. - Updated dependencies [[`e36d954`](https://github.com/clauderic/dnd-kit/commit/e36d95420148659ba78bdbefd3a0a24ec5d02b8f), [`60e7297`](https://github.com/clauderic/dnd-kit/commit/60e72979850bfe4cbb8e2b2e2b8e84bce9edc9f5), [`b7f1cf8`](https://github.com/clauderic/dnd-kit/commit/b7f1cf8f9e15a285c45f896e092f61001335cdff), [`3e629cc`](https://github.com/clauderic/dnd-kit/commit/3e629cc81dbaf9d112c4f1d2c10c75eb6779cf4e), [`8ae7014`](https://github.com/clauderic/dnd-kit/commit/8ae70143bc404bff7678fa8e8390a640c16f2579), [`ce31da7`](https://github.com/clauderic/dnd-kit/commit/ce31da736ec5d4f48bab45430be7b57223d60ee7)]: - @dnd-kit/abstract@0.0.9 - @dnd-kit/geometry@0.0.9 ## 0.0.8 ### Patch Changes - Updated dependencies [[`c9716cf`](https://github.com/clauderic/dnd-kit/commit/c9716cf7b8b846faab451bd2f60c53c77d2d24ba), [`3ea0d31`](https://github.com/clauderic/dnd-kit/commit/3ea0d314649b186bfe0524d50145625da13a8787), [`3cf4db1`](https://github.com/clauderic/dnd-kit/commit/3cf4db126813ebe6ddfc025df5e42e9bfcfa9c38)]: - @dnd-kit/abstract@0.0.8 - @dnd-kit/geometry@0.0.8 ## 0.0.7 ### Patch Changes - Updated dependencies [[`c1dadef`](https://github.com/clauderic/dnd-kit/commit/c1dadef118f8f5f096d36dac314bfe317ea950ce), [`cef9b46`](https://github.com/clauderic/dnd-kit/commit/cef9b46c5ed017e6a601b1d0ee9d0f05b7bbd19f)]: - @dnd-kit/abstract@0.0.7 - @dnd-kit/geometry@0.0.7 ## 0.0.6 ### Patch Changes - [#1567](https://github.com/clauderic/dnd-kit/pull/1567) [`081b7f2`](https://github.com/clauderic/dnd-kit/commit/081b7f2a11da2aad8ce3da7f0579974415d1fdf0) Thanks [@chrisvxd](https://github.com/chrisvxd)! - Add source maps to output. - [#1454](https://github.com/clauderic/dnd-kit/pull/1454) [`69bfad7`](https://github.com/clauderic/dnd-kit/commit/69bfad7d795947987a4281f1a61f81b6a7839fe8) Thanks [@github-actions](https://github.com/apps/github-actions)! - Introduce `closestCorners` collision detection algorithm. - [#1454](https://github.com/clauderic/dnd-kit/pull/1454) [`a8542de`](https://github.com/clauderic/dnd-kit/commit/a8542de56d39c3cd3b6ef981172a0782454295b2) Thanks [@github-actions](https://github.com/apps/github-actions)! - Fix issues with `collisionPriority` not being respected. - [#1454](https://github.com/clauderic/dnd-kit/pull/1454) [`a6366f9`](https://github.com/clauderic/dnd-kit/commit/a6366f9e42836b4c5732141bf314489ede9f60cb) Thanks [@github-actions](https://github.com/apps/github-actions)! - Improve accuracy of `shapeIntersection` when there are multiple intersecting shapes. - Updated dependencies [[`984b5ab`](https://github.com/clauderic/dnd-kit/commit/984b5ab7bec3145dedb9c9b3b560ffbf7e54b919), [`081b7f2`](https://github.com/clauderic/dnd-kit/commit/081b7f2a11da2aad8ce3da7f0579974415d1fdf0), [`a04d3f8`](https://github.com/clauderic/dnd-kit/commit/a04d3f88d380853b97585ab3b608561f7b02ce69), [`a8542de`](https://github.com/clauderic/dnd-kit/commit/a8542de56d39c3cd3b6ef981172a0782454295b2), [`f7458d9`](https://github.com/clauderic/dnd-kit/commit/f7458d9dc32824dbea3a6d5dfb29236f19a2c073), [`e70b29a`](https://github.com/clauderic/dnd-kit/commit/e70b29ae64837e424f7279c95112fb6e420c4dcc), [`4d1a030`](https://github.com/clauderic/dnd-kit/commit/4d1a0306c920ae064eb5b30c4c02961f50460c84), [`a5933d8`](https://github.com/clauderic/dnd-kit/commit/a5933d8607e63ed08818ffab43e858863cb35d47), [`a5a556a`](https://github.com/clauderic/dnd-kit/commit/a5a556abfeec1d78effb3e047f529555e444c020), [`96f28ef`](https://github.com/clauderic/dnd-kit/commit/96f28ef86adf95e77540732d39033c7f3fb0fd04), [`71dc39f`](https://github.com/clauderic/dnd-kit/commit/71dc39fb2ec21b9a680238a91be419c71ecabe86)]: - @dnd-kit/abstract@0.0.6 - @dnd-kit/geometry@0.0.6 ## 0.0.5 ### Patch Changes - Updated dependencies [[`e9be505`](https://github.com/clauderic/dnd-kit/commit/e9be5051b5c99e522fb6efd028d425220b171890)]: - @dnd-kit/abstract@0.0.5 - @dnd-kit/geometry@0.0.5 ## 0.0.4 ### Patch Changes - Updated dependencies [[`2ccc27c`](https://github.com/clauderic/dnd-kit/commit/2ccc27c566b13d6de46719d0ad5978d655261177), [`e0d80f5`](https://github.com/clauderic/dnd-kit/commit/e0d80f59c733b3adcf1fc89d29aa80257e7edd98), [`794cf2f`](https://github.com/clauderic/dnd-kit/commit/794cf2f4bdeeb57a197effb1df654c7c44cf34a3)]: - @dnd-kit/abstract@0.0.4 - @dnd-kit/geometry@0.0.4 ## 0.0.3 ### Patch Changes - Updated dependencies [[`5ccd5e6`](https://github.com/clauderic/dnd-kit/commit/5ccd5e668fb8d736ec3c195116559cb5c5684e80), [`886de33`](https://github.com/clauderic/dnd-kit/commit/886de33d0df851ebdcb3fcf2915f9623069b06d1)]: - @dnd-kit/abstract@0.0.3 - @dnd-kit/geometry@0.0.3 ## 0.0.2 ### Patch Changes - Updated dependencies []: - @dnd-kit/abstract@0.0.2 - @dnd-kit/geometry@0.0.2 ================================================ FILE: packages/collision/package.json ================================================ { "name": "@dnd-kit/collision", "type": "module", "version": "0.3.2", "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", "sideEffects": false, "license": "MIT", "files": [ "dist/**" ], "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" } }, "scripts": { "build": "tsup src/index.ts --format esm,cjs --dts --external react", "dev": "tsup src/index.ts --format esm,cjs --watch --dts --external react", "lint": "TIMING=1 eslint src/**/*.ts* --fix", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, "dependencies": { "@dnd-kit/abstract": "^0.3.2", "@dnd-kit/geometry": "^0.3.2", "tslib": "^2.6.2" }, "devDependencies": { "@types/react": "^18.0.9", "@types/react-dom": "^18.0.4", "eslint": "^8.38.0", "@dnd-kit/eslint-config": "*", "tsup": "8.3.0", "typescript": "^5.5.2" }, "publishConfig": { "access": "public" }, "repository": { "type": "git", "url": "https://github.com/clauderic/dnd-kit" } } ================================================ FILE: packages/collision/src/algorithms/closestCenter.ts ================================================ import {CollisionPriority, CollisionType} from '@dnd-kit/abstract'; import type {CollisionDetector} from '@dnd-kit/abstract'; import {Point} from '@dnd-kit/geometry'; import {defaultCollisionDetection} from './default.ts'; /** * Returns the distance between the droppable shape and the drag operation shape. */ export const closestCenter: CollisionDetector = (input) => { const {dragOperation, droppable} = input; const {shape, position} = dragOperation; if (!droppable.shape) { return null; } const collision = defaultCollisionDetection(input); if (collision) { return collision; } const distance = Point.distance( droppable.shape.center, shape?.current.center ?? position.current ); const value = 1 / distance; return { id: droppable.id, value, type: CollisionType.Collision, priority: CollisionPriority.Normal, }; }; ================================================ FILE: packages/collision/src/algorithms/closestCorners.ts ================================================ import {CollisionPriority, CollisionType} from '@dnd-kit/abstract'; import type {CollisionDetector} from '@dnd-kit/abstract'; import {Point, Rectangle} from '@dnd-kit/geometry'; /** * Returns the distance between the corners of the droppable shape and the drag operation shape. */ export const closestCorners: CollisionDetector = (input) => { const {dragOperation, droppable} = input; const {shape, position} = dragOperation; if (!droppable.shape) { return null; } const shapeCorners = shape ? Rectangle.from(shape.current.boundingRectangle).corners : undefined; const distance = Rectangle.from( droppable.shape.boundingRectangle ).corners.reduce( (acc, corner, index) => acc + Point.distance( Point.from(corner), shapeCorners?.[index] ?? position.current ), 0 ); const value = distance / 4; return { id: droppable.id, value: 1 / value, type: CollisionType.Collision, priority: CollisionPriority.Normal, }; }; ================================================ FILE: packages/collision/src/algorithms/default.ts ================================================ import type {CollisionDetector} from '@dnd-kit/abstract'; import {pointerIntersection} from './pointerIntersection.ts'; import {shapeIntersection} from './shapeIntersection.ts'; /** * Returns the droppable that has the greatest intersection area with the * pointer coordinates. If there are no pointer coordinates, or the pointer * is not intersecting with any droppable, return the greatest intersection area * between the collision shape and other intersecting droppable shapes. */ export const defaultCollisionDetection: CollisionDetector = (args) => { return pointerIntersection(args) ?? shapeIntersection(args); }; ================================================ FILE: packages/collision/src/algorithms/directionBiased.ts ================================================ import {CollisionPriority, CollisionType} from '@dnd-kit/abstract'; import type {CollisionDetector} from '@dnd-kit/abstract'; import {Point} from '@dnd-kit/geometry'; import {defaultCollisionDetection} from './default.ts'; export const directionBiased: CollisionDetector = ({ dragOperation, droppable, }) => { if (!droppable.shape) { return null; } const {position, shape} = dragOperation; const {direction} = position; if (!shape) { return null; } if (direction === null) { return defaultCollisionDetection({dragOperation, droppable}); } const {center} = shape.current; const rect = droppable.shape.boundingRectangle; const isBelow = rect.bottom >= center.y; const isAbove = rect.top <= center.y; const isLeft = rect.left <= center.x; const isRight = rect.right >= center.x; if ( (direction === 'down' && isBelow) || (direction === 'up' && isAbove) || (direction === 'left' && isLeft) || (direction === 'right' && isRight) ) { const distance = Point.distance(droppable.shape.center, center); const value = distance === 0 ? 1 : 1 / distance; return { id: droppable.id, value, type: CollisionType.Collision, priority: CollisionPriority.Normal, }; } return null; }; ================================================ FILE: packages/collision/src/algorithms/index.ts ================================================ export {defaultCollisionDetection} from './default.ts'; export {closestCorners} from './closestCorners.ts'; export {closestCenter} from './closestCenter.ts'; export {pointerIntersection} from './pointerIntersection.ts'; export {shapeIntersection} from './shapeIntersection.ts'; export {directionBiased} from './directionBiased.ts'; export {pointerDistance} from './pointerDistance.ts'; ================================================ FILE: packages/collision/src/algorithms/pointerDistance.ts ================================================ import {CollisionPriority, CollisionType} from '@dnd-kit/abstract'; import type {CollisionDetector} from '@dnd-kit/abstract'; import {Point} from '@dnd-kit/geometry'; /** * Returns the distance between the droppable shape and the drag operation coordinates. */ export const pointerDistance: CollisionDetector = (input) => { const {dragOperation, droppable} = input; const {position} = dragOperation; if (!droppable.shape) { return null; } const distance = Point.distance(droppable.shape.center, position.current); const value = 1 / distance; return { id: droppable.id, value, type: CollisionType.Collision, priority: CollisionPriority.Normal, }; }; ================================================ FILE: packages/collision/src/algorithms/pointerIntersection.ts ================================================ import {CollisionPriority, CollisionType} from '@dnd-kit/abstract'; import type {CollisionDetector} from '@dnd-kit/abstract'; import {Point} from '@dnd-kit/geometry'; /** * A high precision collision detection algorithm that detects * whether the pointer intersects with a given droppable element. * * Returns the distance between the pointer coordinates and the center of the * droppable element if the pointer is within the droppable element. * * Returns null if the pointer is outside of the droppable element. */ export const pointerIntersection: CollisionDetector = ({ dragOperation, droppable, }) => { const pointerCoordinates = dragOperation.position.current; if (!pointerCoordinates) { return null; } const {id} = droppable; if (!droppable.shape) { return null; } if (droppable.shape.containsPoint(pointerCoordinates)) { /* There may be more than a single rectangle intersecting * with the pointer coordinates. In order to sort the * colliding rectangles, we measure the distance between * the pointer and the center of the intersecting rectangle */ const distance = Point.distance(droppable.shape.center, pointerCoordinates); return { id, value: 1 / distance, type: CollisionType.PointerIntersection, priority: CollisionPriority.High, }; } return null; }; ================================================ FILE: packages/collision/src/algorithms/shapeIntersection.ts ================================================ import {CollisionPriority, CollisionType} from '@dnd-kit/abstract'; import type {CollisionDetector} from '@dnd-kit/abstract'; import {Point} from '@dnd-kit/geometry'; /** * Returns the droppable with the greatest intersection area with * the collision shape. */ export const shapeIntersection: CollisionDetector = ({ dragOperation, droppable, }) => { const {shape} = dragOperation; if (!droppable.shape || !shape?.current) { return null; } const intersectionArea = shape.current.intersectionArea(droppable.shape); // Check if the droppable is intersecting with the drag operation shape. if (intersectionArea) { const {position} = dragOperation; /* There could be multiple droppables intersecting with the drag operation shape, * so we need to prioritize the droppable that is the closest to the pointer. * We don't use the intersection area for this because it can lead to cyclic * collisions. */ const distance = Point.distance(droppable.shape.center, position.current); const intersectionRatio = intersectionArea / (shape.current.area + droppable.shape.area - intersectionArea); const value = intersectionRatio / distance; return { id: droppable.id, value, type: CollisionType.ShapeIntersection, priority: CollisionPriority.Normal, }; } return null; }; ================================================ FILE: packages/collision/src/index.ts ================================================ export type {CollisionDetector} from '@dnd-kit/abstract'; export { closestCenter, closestCorners, defaultCollisionDetection, directionBiased, pointerDistance, pointerIntersection, shapeIntersection, } from './algorithms/index.ts'; ================================================ FILE: packages/collision/tsconfig.json ================================================ { "extends": "../../config/typescript/vanilla.json", "include": ["src/**/*"], "exclude": ["dist", "build", "node_modules"] } ================================================ FILE: packages/dom/CHANGELOG.md ================================================ # @dnd-kit/dom ## 0.3.2 ### Patch Changes - [#1903](https://github.com/clauderic/dnd-kit/pull/1903) [`7260746`](https://github.com/clauderic/dnd-kit/commit/7260746b0930d51afb3098ef120bffd7d3aaea03) Thanks [@clauderic](https://github.com/clauderic)! - Fixed CSS cascade layer ordering so that the popover-reset styles injected by the Feedback plugin no longer override styles from CSS frameworks that use cascade layers (such as Tailwind CSS v4). The `@layer` block is now named `dnd-kit` and injected via a `