@octanejs/dnd-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # `@octanejs/dnd-kit`
2
+
3
+ The modern dnd-kit React adapter ported to octane. It reuses dnd-kit's
4
+ framework-agnostic `@dnd-kit/dom` implementation, so sensors, collision
5
+ detection, modifiers, accessibility, feedback, and sortable mechanics stay
6
+ on the official upstream core.
7
+
8
+ ```tsrx
9
+ import { DragDropProvider } from '@octanejs/dnd-kit';
10
+ import { useSortable } from '@octanejs/dnd-kit/sortable';
11
+ import { arrayMove } from '@dnd-kit/helpers';
12
+ import { useState } from 'octane';
13
+
14
+ function Item(props) @{
15
+ const { ref, isDragging } = useSortable({ id: props.id, index: props.index });
16
+ <button ref={ref} data-dragging={isDragging}>{props.id as string}</button>
17
+ }
18
+
19
+ export function App() @{
20
+ const [items, setItems] = useState(['a', 'b', 'c']);
21
+ <DragDropProvider
22
+ onDragEnd={(event) => {
23
+ if (event.canceled || !event.operation.target) return;
24
+ const from = items.indexOf(event.operation.source.id as string);
25
+ const to = items.indexOf(event.operation.target.id as string);
26
+ setItems(arrayMove(items, from, to));
27
+ }}
28
+ >
29
+ @for (const id of items; key id) {
30
+ <Item id={id} index={items.indexOf(id)} />
31
+ }
32
+ </DragDropProvider>
33
+ }
34
+ ```
35
+
36
+ The public entry points mirror `@dnd-kit/react@0.5.0`:
37
+
38
+ - `@octanejs/dnd-kit`
39
+ - `@octanejs/dnd-kit/hooks`
40
+ - `@octanejs/dnd-kit/sortable`
41
+ - `@octanejs/dnd-kit/utilities`
42
+
43
+ This package targets the modern dnd-kit API. The legacy `@dnd-kit/core` 6.x
44
+ API is not included.
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@octanejs/dnd-kit",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
9
+ "octane": {
10
+ "hookSlots": {
11
+ "manual": [
12
+ "src"
13
+ ]
14
+ }
15
+ },
16
+ "description": "dnd-kit bindings for the octane renderer — a full port of the modern @dnd-kit/react adapter over the framework-agnostic @dnd-kit/dom core.",
17
+ "author": {
18
+ "name": "Dominic Gannaway",
19
+ "email": "dg@domgan.com"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/octanejs/octane.git",
27
+ "directory": "packages/dnd-kit"
28
+ },
29
+ "main": "src/index.ts",
30
+ "module": "src/index.ts",
31
+ "types": "src/index.ts",
32
+ "files": [
33
+ "src",
34
+ "README.md"
35
+ ],
36
+ "exports": {
37
+ ".": "./src/index.ts",
38
+ "./hooks": "./src/hooks/index.ts",
39
+ "./sortable": "./src/sortable/index.ts",
40
+ "./utilities": "./src/utilities/index.ts"
41
+ },
42
+ "dependencies": {
43
+ "@dnd-kit/abstract": "0.5.0",
44
+ "@dnd-kit/collision": "0.5.0",
45
+ "@dnd-kit/dom": "0.5.0",
46
+ "@dnd-kit/state": "0.5.0"
47
+ },
48
+ "peerDependencies": {
49
+ "octane": "0.1.5"
50
+ },
51
+ "devDependencies": {
52
+ "@dnd-kit/helpers": "0.5.0",
53
+ "@dnd-kit/react": "0.5.0",
54
+ "@tsrx/react": "^0.2.37",
55
+ "@vitest/coverage-v8": "4.1.9",
56
+ "esbuild": "^0.28.1",
57
+ "react": "^19.2.0",
58
+ "react-dom": "^19.2.0",
59
+ "vitest": "^4.1.9",
60
+ "octane": "0.1.5"
61
+ },
62
+ "scripts": {
63
+ "test": "vitest run --root ../.. --project dnd-kit",
64
+ "test:coverage": "vitest run --root ../.. --project dnd-kit --coverage.enabled --coverage.provider=v8 --coverage.include=packages/dnd-kit/src/** --coverage.reporter=text --coverage.thresholds.lines=100 --coverage.thresholds.statements=98 --coverage.thresholds.functions=98 --coverage.thresholds.branches=86"
65
+ }
66
+ }
@@ -0,0 +1,98 @@
1
+ import { createElement, useEffect, useInsertionEffect, useRef } from 'octane';
2
+ import type { Data, DragDropEventHandlers } from '@dnd-kit/abstract';
3
+ import { DragDropManager, defaultPreset, resolveCustomizable } from '@dnd-kit/dom';
4
+ import type { DragDropManagerInput, Draggable, Droppable } from '@dnd-kit/dom';
5
+ import { deepEqual } from '@dnd-kit/state';
6
+ import { useLatest } from '../../hooks/useLatest';
7
+ import { useOnValueChange } from '../../hooks/useOnValueChange';
8
+ import { DragDropContext } from './context';
9
+ import { Renderer } from './renderer.tsrx';
10
+ import type { OctaneRenderer } from './renderer.tsrx';
11
+
12
+ export type Events<T extends Data = Data, U extends Draggable<T> = Draggable<T>, V extends Droppable<T> = Droppable<T>, W extends DragDropManager<T, U, V> = DragDropManager<T, U, V>> =
13
+ DragDropEventHandlers<U, V, W>;
14
+
15
+ export interface DragDropProviderProps<T extends Data = Data, U extends Draggable<T> = Draggable<T>, V extends Droppable<T> = Droppable<T>, W extends DragDropManager<T, U, V> = DragDropManager<T, U, V>> extends DragDropManagerInput {
16
+ children?: unknown;
17
+ manager?: W;
18
+ onBeforeDragStart?: Events<T, U, V, W>['beforedragstart'];
19
+ onCollision?: Events<T, U, V, W>['collision'];
20
+ onDragStart?: Events<T, U, V, W>['dragstart'];
21
+ onDragMove?: Events<T, U, V, W>['dragmove'];
22
+ onDragOver?: Events<T, U, V, W>['dragover'];
23
+ onDragEnd?: Events<T, U, V, W>['dragend'];
24
+ }
25
+
26
+ const changeOptions = [undefined, deepEqual] as const;
27
+
28
+ export function DragDropProvider<T extends Data = Data, U extends Draggable<T> = Draggable<T>, V extends Droppable<T> = Droppable<T>, W extends DragDropManager<T, U, V> = DragDropManager<T, U, V>>(props: DragDropProviderProps<T, U, V, W>) {
29
+ const {
30
+ children,
31
+ onCollision,
32
+ onBeforeDragStart,
33
+ onDragStart,
34
+ onDragMove,
35
+ onDragOver,
36
+ onDragEnd,
37
+ ...input
38
+ } = props;
39
+ const rendererRef = useRef<OctaneRenderer | null>(null);
40
+ const plugins = resolveCustomizable(input.plugins, defaultPreset.plugins);
41
+ const sensors = resolveCustomizable(input.sensors, defaultPreset.sensors);
42
+ const modifiers = resolveCustomizable(input.modifiers, defaultPreset.modifiers);
43
+ const handleBeforeDragStart = useLatest(onBeforeDragStart);
44
+ const handleDragStart = useLatest(onDragStart);
45
+ const handleDragOver = useLatest(onDragOver);
46
+ const handleDragMove = useLatest(onDragMove);
47
+ const handleDragEnd = useLatest(onDragEnd);
48
+ const handleCollision = useLatest(onCollision);
49
+ const manager = useStableInstance<W>(
50
+ () => input.manager ?? new DragDropManager<T, U, V>(input) as W,
51
+ );
52
+
53
+ useEffect(() => {
54
+ if (!rendererRef.current) throw new Error('Renderer not found');
55
+ const { renderer, trackRendering } = rendererRef.current;
56
+ const { monitor } = manager;
57
+ manager.renderer = renderer;
58
+ const listeners = [
59
+ monitor.addEventListener('beforedragstart', (event) => {
60
+ const callback = handleBeforeDragStart.current;
61
+ if (callback) trackRendering(() => callback(event, manager));
62
+ }),
63
+ monitor.addEventListener('dragstart', (event) => handleDragStart.current?.(event, manager)),
64
+ monitor.addEventListener('dragover', (event) => {
65
+ const callback = handleDragOver.current;
66
+ if (callback) trackRendering(() => callback(event, manager));
67
+ }),
68
+ monitor.addEventListener('dragmove', (event) => {
69
+ const callback = handleDragMove.current;
70
+ if (callback) trackRendering(() => callback(event, manager));
71
+ }),
72
+ monitor.addEventListener('dragend', (event) => {
73
+ const callback = handleDragEnd.current;
74
+ if (callback) trackRendering(() => callback(event, manager));
75
+ }),
76
+ monitor.addEventListener('collision', (event) => handleCollision.current?.(event, manager)),
77
+ ];
78
+ return () => listeners.forEach((dispose) => dispose());
79
+ }, [manager]);
80
+
81
+ useOnValueChange(plugins, () => (manager.plugins = plugins), ...changeOptions);
82
+ useOnValueChange(sensors, () => (manager.sensors = sensors), ...changeOptions);
83
+ useOnValueChange(modifiers, () => (manager.modifiers = modifiers), ...changeOptions);
84
+
85
+ return createElement(
86
+ DragDropContext.Provider,
87
+ { value: manager as DragDropManager },
88
+ createElement(Renderer, { ref: rendererRef }, children),
89
+ children,
90
+ );
91
+ }
92
+
93
+ function useStableInstance<T extends { destroy(): void }>(create: () => T): T {
94
+ const ref = useRef<T | null>(null);
95
+ if (!ref.current) ref.current = create();
96
+ useInsertionEffect(() => () => ref.current?.destroy(), []);
97
+ return ref.current;
98
+ }
@@ -0,0 +1,32 @@
1
+ import type { Data, DragDropEventHandlers } from '@dnd-kit/abstract';
2
+ import type { DragDropManager, DragDropManagerInput, Draggable, Droppable } from '@dnd-kit/dom';
3
+
4
+ export type Events<
5
+ T extends Data = Data,
6
+ U extends Draggable<T> = Draggable<T>,
7
+ V extends Droppable<T> = Droppable<T>,
8
+ W extends DragDropManager<T, U, V> = DragDropManager<T, U, V>,
9
+ > = DragDropEventHandlers<U, V, W>;
10
+
11
+ export interface DragDropProviderProps<
12
+ T extends Data = Data,
13
+ U extends Draggable<T> = Draggable<T>,
14
+ V extends Droppable<T> = Droppable<T>,
15
+ W extends DragDropManager<T, U, V> = DragDropManager<T, U, V>,
16
+ > extends DragDropManagerInput {
17
+ children?: unknown;
18
+ manager?: W;
19
+ onBeforeDragStart?: Events<T, U, V, W>['beforedragstart'];
20
+ onCollision?: Events<T, U, V, W>['collision'];
21
+ onDragStart?: Events<T, U, V, W>['dragstart'];
22
+ onDragMove?: Events<T, U, V, W>['dragmove'];
23
+ onDragOver?: Events<T, U, V, W>['dragover'];
24
+ onDragEnd?: Events<T, U, V, W>['dragend'];
25
+ }
26
+
27
+ export declare function DragDropProvider<
28
+ T extends Data = Data,
29
+ U extends Draggable<T> = Draggable<T>,
30
+ V extends Droppable<T> = Droppable<T>,
31
+ W extends DragDropManager<T, U, V> = DragDropManager<T, U, V>,
32
+ >(props: DragDropProviderProps<T, U, V, W>): unknown;
@@ -0,0 +1,4 @@
1
+ import { createContext } from 'octane';
2
+ import type { DragDropManager } from '@dnd-kit/dom';
3
+
4
+ export const DragDropContext = createContext<DragDropManager | null>(null);
@@ -0,0 +1,44 @@
1
+ import { memo, startTransition, useImperativeHandle, useMemo, useRef, useState } from 'octane';
2
+ import type { Renderer as AbstractRenderer } from '@dnd-kit/abstract';
3
+ import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect';
4
+
5
+ export type OctaneRenderer = {
6
+ renderer: AbstractRenderer;
7
+ trackRendering: (callback: () => void) => void;
8
+ };
9
+
10
+ function RendererImpl(props: { children: unknown; ref?: { current: OctaneRenderer | null } }) {
11
+ const [transitionCount, setTransitionCount] = useState(0);
12
+ const rendering = useRef<Promise<void> | null>(null);
13
+ const resolver = useRef<(() => void) | null>(null);
14
+ const renderer = useMemo<OctaneRenderer>(
15
+ () => ({
16
+ renderer: {
17
+ get rendering() {
18
+ return rendering.current ?? Promise.resolve();
19
+ },
20
+ },
21
+ trackRendering(callback: () => void) {
22
+ if (!rendering.current) {
23
+ rendering.current = new Promise<void>((resolve) => {
24
+ resolver.current = resolve;
25
+ });
26
+ }
27
+ startTransition(() => {
28
+ callback();
29
+ setTransitionCount((count) => count + 1);
30
+ });
31
+ },
32
+ }),
33
+ [],
34
+ );
35
+
36
+ useIsomorphicLayoutEffect(() => {
37
+ resolver.current?.();
38
+ rendering.current = null;
39
+ }, [props.children, transitionCount]);
40
+ useImperativeHandle(props.ref, () => renderer);
41
+ return null;
42
+ }
43
+
44
+ export const Renderer = memo(RendererImpl);
@@ -0,0 +1,91 @@
1
+ import { createElement, isChildrenBlock, useEffect, useMemo, useRef } from 'octane';
2
+ import type { Data } from '@dnd-kit/abstract';
3
+ import { DragDropManager, Draggable, Feedback } from '@dnd-kit/dom';
4
+ import type { DropAnimation } from '@dnd-kit/dom';
5
+ import { useComputed } from '../../hooks/useComputed';
6
+ import { useDeepSignal } from '../../hooks/useDeepSignal';
7
+ import { DragDropContext } from '../context/context';
8
+ import { useDragDropManager } from '../hooks/useDragDropManager';
9
+
10
+ type DragOverlayChild = object | string | number | bigint | boolean | null | undefined;
11
+
12
+ export interface DragOverlayProps<T extends Data, U extends Draggable<T>> {
13
+ className?: string;
14
+ children: DragOverlayChild | ((source: U) => DragOverlayChild);
15
+ dropAnimation?: DropAnimation | null;
16
+ style?: Record<string, string | number | null | undefined>;
17
+ tag?: string;
18
+ disabled?: boolean | ((source: U | null) => boolean);
19
+ }
20
+
21
+ export function DragOverlay<T extends Data, U extends Draggable<T>>(props: DragOverlayProps<T, U>) {
22
+ const { children, className, dropAnimation, style, tag, disabled } = props;
23
+ const ref = useRef<HTMLElement | null>(null);
24
+ const manager = useDragDropManager<T, U>();
25
+ const source = useComputed(() => manager?.dragOperation.source, [manager]).value ?? null;
26
+ const isDisabled =
27
+ typeof disabled === 'function' ? disabled(source) : disabled;
28
+
29
+ useEffect(() => {
30
+ if (!ref.current || !manager || isDisabled) return;
31
+ const feedback = manager.plugins.find((plugin) => plugin instanceof Feedback);
32
+ if (!feedback) return;
33
+ feedback.overlay = ref.current;
34
+ return () => {
35
+ feedback.overlay = undefined;
36
+ };
37
+ }, [manager, isDisabled]);
38
+
39
+ useEffect(() => {
40
+ if (!manager) return;
41
+ const feedback = manager.plugins.find((plugin) => plugin instanceof Feedback);
42
+ if (!feedback) return;
43
+ feedback.dropAnimation = dropAnimation;
44
+ return () => {
45
+ feedback.dropAnimation = undefined;
46
+ };
47
+ }, [manager, dropAnimation]);
48
+
49
+ const patchedManager = useMemo(() => {
50
+ if (!manager) return null;
51
+ const patchedRegistry = new Proxy(manager.registry, {
52
+ get(target, property) {
53
+ if (property === 'register' || property === 'unregister') return noop;
54
+ return target[property as keyof typeof target];
55
+ },
56
+ });
57
+ return new Proxy(manager, {
58
+ get(target, property) {
59
+ if (property === 'registry') return patchedRegistry;
60
+ return target[property as keyof typeof target];
61
+ },
62
+ });
63
+ }, [manager]);
64
+
65
+ let content: unknown = null;
66
+ if (source && !isDisabled) {
67
+ content = typeof children === 'function' && !isChildrenBlock(children)
68
+ ? createElement(OverlayChildren, { source, children })
69
+ : children;
70
+ }
71
+
72
+ return createElement(DragDropContext.Provider, {
73
+ value: patchedManager as DragDropManager | null,
74
+ children: createElement(
75
+ tag || 'div',
76
+ { ref, className, style, 'data-dnd-overlay': true },
77
+ content,
78
+ ),
79
+ });
80
+ }
81
+
82
+ function noop() {
83
+ return () => {};
84
+ }
85
+
86
+ function OverlayChildren<T extends Data, U extends Draggable<T>>(props: {
87
+ children: (source: U) => DragOverlayChild;
88
+ source: U;
89
+ }) {
90
+ return props.children(useDeepSignal(props.source));
91
+ }
@@ -0,0 +1,17 @@
1
+ import type { Data } from '@dnd-kit/abstract';
2
+ import type { Draggable, DropAnimation } from '@dnd-kit/dom';
3
+
4
+ type DragOverlayChild = object | string | number | bigint | boolean | null | undefined;
5
+
6
+ export interface DragOverlayProps<T extends Data, U extends Draggable<T>> {
7
+ className?: string;
8
+ children: DragOverlayChild | ((source: U) => DragOverlayChild);
9
+ dropAnimation?: DropAnimation | null;
10
+ style?: Record<string, string | number | null | undefined>;
11
+ tag?: string;
12
+ disabled?: boolean | ((source: U | null) => boolean);
13
+ }
14
+
15
+ export declare function DragOverlay<T extends Data, U extends Draggable<T>>(
16
+ props: DragOverlayProps<T, U>,
17
+ ): unknown;
@@ -0,0 +1,111 @@
1
+ import { useCallback } from 'octane';
2
+ import type { Data } from '@dnd-kit/abstract';
3
+ import { deepEqual } from '@dnd-kit/state';
4
+ import { Draggable } from '@dnd-kit/dom';
5
+ import type { DraggableInput } from '@dnd-kit/dom';
6
+ import { subSlot } from '../../internal';
7
+ import { useDeepSignal } from '../../hooks/useDeepSignal';
8
+ import { useOnElementChange } from '../../hooks/useOnElementChange';
9
+ import { useOnValueChange } from '../../hooks/useOnValueChange';
10
+ import { currentValue, type RefOrValue } from '../../utilities/currentValue';
11
+ import { useInstance } from '../hooks/useInstance';
12
+
13
+ export interface UseDraggableInput<T extends Data = Data> extends Omit<
14
+ DraggableInput<T>,
15
+ 'handle' | 'element'
16
+ > {
17
+ handle?: RefOrValue<Element>;
18
+ element?: RefOrValue<Element>;
19
+ }
20
+
21
+ export function useDraggable<T extends Data = Data>(input: UseDraggableInput<T>, slot?: symbol) {
22
+ const { disabled, data, element, handle, id, modifiers, sensors, plugins } = input;
23
+ const draggable = useInstance(
24
+ (manager) =>
25
+ new Draggable(
26
+ {
27
+ ...input,
28
+ register: false,
29
+ handle: currentValue(handle),
30
+ element: currentValue(element),
31
+ },
32
+ manager,
33
+ ),
34
+ subSlot(slot, 'instance'),
35
+ );
36
+ const tracked = useDeepSignal(draggable, shouldUpdateSynchronously, subSlot(slot, 'signal'));
37
+
38
+ useOnValueChange(id, () => (draggable.id = id), subSlot(slot, 'id'));
39
+ useOnElementChange(handle, (value) => (draggable.handle = value), subSlot(slot, 'handle'));
40
+ useOnElementChange(element, (value) => (draggable.element = value), subSlot(slot, 'element'));
41
+ useOnValueChange(data, () => data && (draggable.data = data), subSlot(slot, 'data'));
42
+ useOnValueChange(
43
+ disabled,
44
+ () => (draggable.disabled = disabled === true),
45
+ subSlot(slot, 'disabled'),
46
+ );
47
+ useOnValueChange(
48
+ sensors,
49
+ () => (draggable.sensors = sensors),
50
+ undefined,
51
+ deepEqual,
52
+ subSlot(slot, 'sensors'),
53
+ );
54
+ useOnValueChange(
55
+ modifiers,
56
+ () => (draggable.modifiers = modifiers),
57
+ undefined,
58
+ deepEqual,
59
+ subSlot(slot, 'modifiers'),
60
+ );
61
+ useOnValueChange(
62
+ plugins,
63
+ () => (draggable.plugins = plugins),
64
+ undefined,
65
+ deepEqual,
66
+ subSlot(slot, 'plugins'),
67
+ );
68
+ useOnValueChange(
69
+ input.alignment,
70
+ () => (draggable.alignment = input.alignment),
71
+ subSlot(slot, 'alignment'),
72
+ );
73
+
74
+ return {
75
+ draggable: tracked,
76
+ get isDragging() {
77
+ return tracked.isDragging;
78
+ },
79
+ get isDropping() {
80
+ return tracked.isDropping;
81
+ },
82
+ get isDragSource() {
83
+ return tracked.isDragSource;
84
+ },
85
+ handleRef: useCallback(
86
+ (value: Element | null) => {
87
+ draggable.handle = value ?? undefined;
88
+ },
89
+ [draggable],
90
+ subSlot(slot, 'handle-ref'),
91
+ ),
92
+ ref: useCallback(
93
+ (value: Element | null) => {
94
+ if (
95
+ !value &&
96
+ draggable.element?.isConnected &&
97
+ !draggable.manager?.dragOperation.status.idle
98
+ ) {
99
+ return;
100
+ }
101
+ draggable.element = value ?? undefined;
102
+ },
103
+ [draggable],
104
+ subSlot(slot, 'ref'),
105
+ ),
106
+ };
107
+ }
108
+
109
+ function shouldUpdateSynchronously(key: string, oldValue: any, newValue: any): boolean {
110
+ return key === 'isDragSource' && !newValue && oldValue;
111
+ }
@@ -0,0 +1,79 @@
1
+ import { useCallback } from 'octane';
2
+ import type { Data } from '@dnd-kit/abstract';
3
+ import { defaultCollisionDetection } from '@dnd-kit/collision';
4
+ import { Droppable } from '@dnd-kit/dom';
5
+ import type { DroppableInput } from '@dnd-kit/dom';
6
+ import { deepEqual } from '@dnd-kit/state';
7
+ import { subSlot } from '../../internal';
8
+ import { useDeepSignal } from '../../hooks/useDeepSignal';
9
+ import { useOnElementChange } from '../../hooks/useOnElementChange';
10
+ import { useOnValueChange } from '../../hooks/useOnValueChange';
11
+ import { currentValue, type RefOrValue } from '../../utilities/currentValue';
12
+ import { useInstance } from '../hooks/useInstance';
13
+
14
+ export interface UseDroppableInput<T extends Data = Data> extends Omit<
15
+ DroppableInput<T>,
16
+ 'element'
17
+ > {
18
+ element?: RefOrValue<Element>;
19
+ }
20
+
21
+ export function useDroppable<T extends Data = Data>(input: UseDroppableInput<T>, slot?: symbol) {
22
+ const { collisionDetector, data, disabled, element, id, accept, type } = input;
23
+ const droppable = useInstance(
24
+ (manager) =>
25
+ new Droppable(
26
+ {
27
+ ...input,
28
+ register: false,
29
+ element: currentValue(element),
30
+ },
31
+ manager,
32
+ ),
33
+ subSlot(slot, 'instance'),
34
+ );
35
+ const tracked = useDeepSignal(droppable, subSlot(slot, 'signal'));
36
+
37
+ useOnValueChange(id, () => (droppable.id = id), subSlot(slot, 'id'));
38
+ useOnElementChange(element, (value) => (droppable.element = value), subSlot(slot, 'element'));
39
+ useOnValueChange(
40
+ accept,
41
+ () => (droppable.accept = accept),
42
+ undefined,
43
+ deepEqual,
44
+ subSlot(slot, 'accept'),
45
+ );
46
+ useOnValueChange(
47
+ collisionDetector,
48
+ () => (droppable.collisionDetector = collisionDetector ?? defaultCollisionDetection),
49
+ subSlot(slot, 'collision'),
50
+ );
51
+ useOnValueChange(data, () => data && (droppable.data = data), subSlot(slot, 'data'));
52
+ useOnValueChange(
53
+ disabled,
54
+ () => (droppable.disabled = disabled === true),
55
+ subSlot(slot, 'disabled'),
56
+ );
57
+ useOnValueChange(type, () => (droppable.type = type), subSlot(slot, 'type'));
58
+
59
+ return {
60
+ droppable: tracked,
61
+ get isDropTarget() {
62
+ return tracked.isDropTarget;
63
+ },
64
+ ref: useCallback(
65
+ (value: Element | null) => {
66
+ if (
67
+ !value &&
68
+ droppable.element?.isConnected &&
69
+ !droppable.manager?.dragOperation.status.idle
70
+ ) {
71
+ return;
72
+ }
73
+ droppable.element = value ?? undefined;
74
+ },
75
+ [droppable],
76
+ subSlot(slot, 'ref'),
77
+ ),
78
+ };
79
+ }
@@ -0,0 +1,13 @@
1
+ import { useContext } from 'octane';
2
+ import type { Data } from '@dnd-kit/abstract';
3
+ import type { Draggable, Droppable, DragDropManager } from '@dnd-kit/dom';
4
+ import { DragDropContext } from '../context/context';
5
+
6
+ export function useDragDropManager<
7
+ T extends Data = Data,
8
+ U extends Draggable<T> = Draggable<T>,
9
+ V extends Droppable<T> = Droppable<T>,
10
+ W extends DragDropManager<T, U, V> = DragDropManager<T, U, V>,
11
+ >(_slot?: symbol): W | null {
12
+ return useContext(DragDropContext) as W | null;
13
+ }
@@ -0,0 +1,69 @@
1
+ import { useEffect } from 'octane';
2
+ import type { DragDropEventHandlers, Data } from '@dnd-kit/abstract';
3
+ import type { Draggable, Droppable, DragDropManager } from '@dnd-kit/dom';
4
+ import type { CleanupFunction } from '@dnd-kit/state';
5
+ import { subSlot } from '../../internal';
6
+ import { useDragDropManager } from './useDragDropManager';
7
+
8
+ type EventNameOverrides = { beforedragstart: 'onBeforeDragStart' };
9
+ type EventHandlerName<T extends string> = T extends keyof EventNameOverrides
10
+ ? EventNameOverrides[T]
11
+ : T extends `drag${infer Second}${infer Rest}`
12
+ ? `onDrag${Uppercase<Second>}${Rest}`
13
+ : `on${Capitalize<T>}`;
14
+
15
+ type Events<
16
+ T extends Data,
17
+ U extends Draggable<T>,
18
+ V extends Droppable<T>,
19
+ W extends DragDropManager<T, U, V>,
20
+ > = DragDropEventHandlers<U, V, W>;
21
+
22
+ export type EventHandlers<
23
+ T extends Data = Data,
24
+ U extends Draggable<T> = Draggable<T>,
25
+ V extends Droppable<T> = Droppable<T>,
26
+ W extends DragDropManager<T, U, V> = DragDropManager<T, U, V>,
27
+ > = {
28
+ [K in keyof Events<T, U, V, W> as EventHandlerName<K>]: Events<T, U, V, W>[K];
29
+ };
30
+
31
+ export function useDragDropMonitor<
32
+ T extends Data = Data,
33
+ U extends Draggable<T> = Draggable<T>,
34
+ V extends Droppable<T> = Droppable<T>,
35
+ W extends DragDropManager<T, U, V> = DragDropManager<T, U, V>,
36
+ >(handlers: Partial<EventHandlers<T, U, V, W>>, slot?: symbol): void {
37
+ const manager = useDragDropManager<T, U, V, W>();
38
+ useEffect(
39
+ () => {
40
+ if (!manager) {
41
+ if (process.env.NODE_ENV !== 'production') {
42
+ console.warn(
43
+ 'useDragDropMonitor hook was called outside of a DragDropProvider. ' +
44
+ 'Make sure your app is wrapped in a DragDropProvider component.',
45
+ );
46
+ }
47
+ return;
48
+ }
49
+ const cleanup = Object.entries(handlers).reduce<CleanupFunction[]>(
50
+ (entries, [handlerName, handler]) => {
51
+ if (handler) {
52
+ const eventName = handlerName.replace(/^on/, '').toLowerCase() as keyof Events<
53
+ T,
54
+ U,
55
+ V,
56
+ W
57
+ >;
58
+ entries.push(manager.monitor.addEventListener(eventName, handler as any));
59
+ }
60
+ return entries;
61
+ },
62
+ [],
63
+ );
64
+ return () => cleanup.forEach((dispose) => dispose());
65
+ },
66
+ [manager, handlers],
67
+ subSlot(slot, 'effect'),
68
+ );
69
+ }
@@ -0,0 +1,34 @@
1
+ import type { Data } from '@dnd-kit/abstract';
2
+ import type { Draggable, Droppable, DragDropManager } from '@dnd-kit/dom';
3
+ import { subSlot } from '../../internal';
4
+ import { useComputed } from '../../hooks/useComputed';
5
+ import { useDragDropManager } from './useDragDropManager';
6
+
7
+ export function useDragOperation<
8
+ T extends Data = Data,
9
+ U extends Draggable<T> = Draggable<T>,
10
+ V extends Droppable<T> = Droppable<T>,
11
+ W extends DragDropManager<T, U, V> = DragDropManager<T, U, V>,
12
+ >(slot?: symbol): { readonly source: U | null | undefined; readonly target: V | null | undefined } {
13
+ const manager = useDragDropManager<T, U, V, W>();
14
+ const source = useComputed(
15
+ () => manager?.dragOperation.source,
16
+ [manager],
17
+ false,
18
+ subSlot(slot, 'source'),
19
+ );
20
+ const target = useComputed(
21
+ () => manager?.dragOperation.target,
22
+ [manager],
23
+ false,
24
+ subSlot(slot, 'target'),
25
+ );
26
+ return {
27
+ get source() {
28
+ return source.value as U | null | undefined;
29
+ },
30
+ get target() {
31
+ return target.value as V | null | undefined;
32
+ },
33
+ };
34
+ }
@@ -0,0 +1,22 @@
1
+ import { useState } from 'octane';
2
+ import type { DragDropManager } from '@dnd-kit/abstract';
3
+ import type { CleanupFunction } from '@dnd-kit/state';
4
+ import { subSlot } from '../../internal';
5
+ import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect';
6
+ import { useDragDropManager } from './useDragDropManager';
7
+
8
+ export interface Instance<T extends DragDropManager<any, any> = DragDropManager<any, any>> {
9
+ manager: T | undefined;
10
+ register(): CleanupFunction | void;
11
+ }
12
+
13
+ export function useInstance<T extends Instance>(
14
+ initializer: (manager: DragDropManager<any, any> | undefined) => T,
15
+ slot?: symbol,
16
+ ): T {
17
+ const manager = useDragDropManager() ?? undefined;
18
+ const [instance] = useState<T>(() => initializer(manager), subSlot(slot, 'instance'));
19
+ if (instance.manager !== manager) instance.manager = manager;
20
+ useIsomorphicLayoutEffect(instance.register, [manager, instance], subSlot(slot, 'register'));
21
+ return instance;
22
+ }
@@ -0,0 +1,21 @@
1
+ export { DragDropProvider, type DragDropProviderProps } from './context/DragDropProvider.tsrx';
2
+ export { useDraggable, type UseDraggableInput } from './draggable/useDraggable';
3
+ export { DragOverlay, type DragOverlayProps } from './draggable/DragOverlay.tsrx';
4
+ export { useDroppable, type UseDroppableInput } from './droppable/useDroppable';
5
+ export { useDragDropManager } from './hooks/useDragDropManager';
6
+ export {
7
+ useDragDropMonitor,
8
+ type EventHandlers as DragDropEventHandlers,
9
+ } from './hooks/useDragDropMonitor';
10
+ export { useDragOperation } from './hooks/useDragOperation';
11
+ export { useInstance } from './hooks/useInstance';
12
+ export { KeyboardSensor, PointerSensor } from '@dnd-kit/dom';
13
+ export type {
14
+ DragDropManager,
15
+ CollisionEvent,
16
+ BeforeDragStartEvent,
17
+ DragStartEvent,
18
+ DragMoveEvent,
19
+ DragOverEvent,
20
+ DragEndEvent,
21
+ } from '@dnd-kit/dom';
@@ -0,0 +1,8 @@
1
+ export { useConstant } from './useConstant';
2
+ export { useComputed } from './useComputed';
3
+ export { useDeepSignal } from './useDeepSignal';
4
+ export { useImmediateEffect } from './useImmediateEffect';
5
+ export { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
6
+ export { useLatest } from './useLatest';
7
+ export { useOnValueChange } from './useOnValueChange';
8
+ export { useOnElementChange } from './useOnElementChange';
@@ -0,0 +1,28 @@
1
+ import { useMemo, useRef } from 'octane';
2
+ import { computed, type Signal } from '@dnd-kit/state';
3
+ import { subSlot } from '../internal';
4
+ import { useSignal } from './useSignal';
5
+
6
+ export function useComputed<T = any>(
7
+ compute: () => T,
8
+ dependenciesOrSlot: any[] | symbol = [],
9
+ synchronousOrSlot: boolean | symbol = false,
10
+ maybeSlot?: symbol,
11
+ ): { readonly value: T } {
12
+ const slot =
13
+ typeof dependenciesOrSlot === 'symbol'
14
+ ? dependenciesOrSlot
15
+ : typeof synchronousOrSlot === 'symbol'
16
+ ? synchronousOrSlot
17
+ : maybeSlot;
18
+ const dependencies = Array.isArray(dependenciesOrSlot) ? dependenciesOrSlot : [];
19
+ const synchronous = typeof synchronousOrSlot === 'boolean' ? synchronousOrSlot : false;
20
+ const computeRef = useRef(compute, subSlot(slot, 'compute'));
21
+ computeRef.current = compute;
22
+ const value = useMemo(
23
+ () => computed(() => computeRef.current()),
24
+ dependencies,
25
+ subSlot(slot, 'memo'),
26
+ );
27
+ return useSignal(value as Signal<T>, synchronous, subSlot(slot, 'signal'));
28
+ }
@@ -0,0 +1,7 @@
1
+ import { useRef } from 'octane';
2
+
3
+ export function useConstant<T = any>(initializer: () => T, slot?: symbol): T {
4
+ const ref = useRef<T | null>(null, slot);
5
+ if (ref.current === null) ref.current = initializer();
6
+ return ref.current;
7
+ }
@@ -0,0 +1,61 @@
1
+ import { flushSync, useMemo, useRef } from 'octane';
2
+ import { effect, untracked } from '@dnd-kit/state';
3
+ import { subSlot } from '../internal';
4
+ import { useForceUpdate } from './useForceUpdate';
5
+ import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
6
+
7
+ type Synchronous<T> = (property: keyof T, oldValue: any, newValue: any) => boolean;
8
+
9
+ export function useDeepSignal<T extends object | null | undefined>(
10
+ target: T,
11
+ synchronousOrSlot?: Synchronous<NonNullable<T>> | symbol,
12
+ maybeSlot?: symbol,
13
+ ): T {
14
+ const slot = typeof synchronousOrSlot === 'symbol' ? synchronousOrSlot : maybeSlot;
15
+ const synchronous = typeof synchronousOrSlot === 'function' ? synchronousOrSlot : undefined;
16
+ const tracked = useRef(new Map<string | symbol, any>(), subSlot(slot, 'tracked'));
17
+ const forceUpdate = useForceUpdate(subSlot(slot, 'force'));
18
+
19
+ useIsomorphicLayoutEffect(
20
+ () => {
21
+ if (!target) {
22
+ tracked.current.clear();
23
+ return;
24
+ }
25
+ return effect(() => {
26
+ let stale = false;
27
+ let sync = false;
28
+ for (const [key, previous] of tracked.current) {
29
+ const value = untracked(() => previous);
30
+ const latestValue = (target as any)[key];
31
+ if (value !== latestValue) {
32
+ stale = true;
33
+ tracked.current.set(key, latestValue);
34
+ sync = synchronous?.(key as keyof NonNullable<T>, value, latestValue) ?? false;
35
+ }
36
+ }
37
+ if (stale) {
38
+ if (sync) queueMicrotask(() => flushSync(forceUpdate));
39
+ else forceUpdate();
40
+ }
41
+ });
42
+ },
43
+ [target],
44
+ subSlot(slot, 'effect'),
45
+ );
46
+
47
+ return useMemo(
48
+ () =>
49
+ target
50
+ ? new Proxy(target, {
51
+ get(value, key) {
52
+ const current = (value as any)[key];
53
+ tracked.current.set(key, current);
54
+ return current;
55
+ },
56
+ })
57
+ : target,
58
+ [target],
59
+ subSlot(slot, 'memo'),
60
+ );
61
+ }
@@ -0,0 +1,13 @@
1
+ import { useCallback, useState } from 'octane';
2
+ import { subSlot } from '../internal';
3
+
4
+ export function useForceUpdate(slot?: symbol): () => void {
5
+ const setState = useState(0, subSlot(slot, 'state'))[1];
6
+ return useCallback(
7
+ () => {
8
+ setState((value) => value + 1);
9
+ },
10
+ [],
11
+ subSlot(slot, 'callback'),
12
+ );
13
+ }
@@ -0,0 +1,6 @@
1
+ export function useImmediateEffect(
2
+ callback: () => void | (() => void),
3
+ _dependencies?: any[] | null,
4
+ ): void {
5
+ callback();
6
+ }
@@ -0,0 +1,17 @@
1
+ import { useEffect, useLayoutEffect } from 'octane';
2
+ import { canUseDOM } from '@dnd-kit/dom/utilities';
3
+
4
+ type EffectCallback = () => void | (() => void);
5
+ type EffectHook = (callback: EffectCallback, deps?: any[] | null, slot?: symbol) => void;
6
+
7
+ const effect: EffectHook = canUseDOM ? useLayoutEffect : useEffect;
8
+
9
+ export function useIsomorphicLayoutEffect(
10
+ callback: EffectCallback,
11
+ dependenciesOrSlot?: any[] | null | symbol,
12
+ maybeSlot?: symbol,
13
+ ): void {
14
+ const slot = typeof dependenciesOrSlot === 'symbol' ? dependenciesOrSlot : maybeSlot;
15
+ const dependencies = typeof dependenciesOrSlot === 'symbol' ? undefined : dependenciesOrSlot;
16
+ effect(callback, dependencies, slot);
17
+ }
@@ -0,0 +1,15 @@
1
+ import { useRef } from 'octane';
2
+ import { subSlot } from '../internal';
3
+ import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
4
+
5
+ export function useLatest<T>(value: T, slot?: symbol): { current: T | undefined } {
6
+ const valueRef = useRef<T | undefined>(value, subSlot(slot, 'ref'));
7
+ useIsomorphicLayoutEffect(
8
+ () => {
9
+ valueRef.current = value;
10
+ },
11
+ undefined,
12
+ subSlot(slot, 'effect'),
13
+ );
14
+ return valueRef;
15
+ }
@@ -0,0 +1,23 @@
1
+ import { useRef } from 'octane';
2
+ import { currentValue, type RefOrValue } from '../utilities/currentValue';
3
+ import { subSlot } from '../internal';
4
+ import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
5
+
6
+ export function useOnElementChange(
7
+ value: RefOrValue<Element>,
8
+ onChange: (value: Element | undefined) => void,
9
+ slot?: symbol,
10
+ ): void {
11
+ const previous = useRef(currentValue(value), subSlot(slot, 'previous'));
12
+ useIsomorphicLayoutEffect(
13
+ () => {
14
+ const current = currentValue(value);
15
+ if (current !== previous.current) {
16
+ previous.current = current;
17
+ onChange(current);
18
+ }
19
+ },
20
+ undefined,
21
+ subSlot(slot, 'effect'),
22
+ );
23
+ }
@@ -0,0 +1,38 @@
1
+ import { useEffect, useRef } from 'octane';
2
+ import { subSlot } from '../internal';
3
+
4
+ type EffectHook = (
5
+ callback: () => void | (() => void),
6
+ dependencies?: any[] | null,
7
+ slot?: symbol,
8
+ ) => void;
9
+
10
+ export function useOnValueChange<T>(
11
+ value: T,
12
+ onChange: (value: T, oldValue: T) => void,
13
+ effectOrSlot: EffectHook | symbol | undefined = useEffect,
14
+ compareOrSlot: ((a: T, b: T) => boolean) | symbol | undefined = Object.is,
15
+ maybeSlot?: symbol,
16
+ ): void {
17
+ const slot =
18
+ typeof effectOrSlot === 'symbol'
19
+ ? effectOrSlot
20
+ : typeof compareOrSlot === 'symbol'
21
+ ? compareOrSlot
22
+ : maybeSlot;
23
+ const effect = typeof effectOrSlot === 'function' ? effectOrSlot : useEffect;
24
+ const compare = typeof compareOrSlot === 'function' ? compareOrSlot : Object.is;
25
+ const tracked = useRef<T>(value, subSlot(slot, 'tracked'));
26
+
27
+ effect(
28
+ () => {
29
+ const oldValue = tracked.current;
30
+ if (!compare(value, oldValue)) {
31
+ tracked.current = value;
32
+ onChange(value, oldValue);
33
+ }
34
+ },
35
+ [onChange, value],
36
+ subSlot(slot, 'effect'),
37
+ );
38
+ }
@@ -0,0 +1,40 @@
1
+ import { flushSync, useRef } from 'octane';
2
+ import { effect, type Signal } from '@dnd-kit/state';
3
+ import { subSlot } from '../internal';
4
+ import { useForceUpdate } from './useForceUpdate';
5
+ import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
6
+
7
+ export function useSignal<T = any>(
8
+ signal: Signal<T>,
9
+ synchronousOrSlot: boolean | symbol = false,
10
+ maybeSlot?: symbol,
11
+ ): { readonly value: T } {
12
+ const slot = typeof synchronousOrSlot === 'symbol' ? synchronousOrSlot : maybeSlot;
13
+ const synchronous = typeof synchronousOrSlot === 'boolean' ? synchronousOrSlot : false;
14
+ const previous = useRef(signal.peek(), subSlot(slot, 'previous'));
15
+ const read = useRef(false, subSlot(slot, 'read'));
16
+ const forceUpdate = useForceUpdate(subSlot(slot, 'force'));
17
+
18
+ useIsomorphicLayoutEffect(
19
+ () =>
20
+ effect(() => {
21
+ const previousValue = previous.current;
22
+ const currentValue = signal.value;
23
+ if (previousValue !== currentValue) {
24
+ previous.current = currentValue;
25
+ if (!read.current) return;
26
+ if (synchronous) flushSync(forceUpdate);
27
+ else forceUpdate();
28
+ }
29
+ }),
30
+ [signal, synchronous, forceUpdate],
31
+ subSlot(slot, 'effect'),
32
+ );
33
+
34
+ return {
35
+ get value() {
36
+ read.current = true;
37
+ return signal.peek();
38
+ },
39
+ };
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './core/index';
@@ -0,0 +1,17 @@
1
+ // The package ships raw TypeScript. Public custom hooks receive the compiler's
2
+ // trailing call-site slot and derive distinct slots for every base hook they
3
+ // compose. This keeps multiple draggable/sortable hooks in one component
4
+ // isolated while preserving upstream's stable hook identities.
5
+ const subSlotCache = new Map<symbol, Map<string, symbol>>();
6
+
7
+ export function subSlot(slot: symbol | undefined, tag: string): symbol | undefined {
8
+ if (slot === undefined) return undefined;
9
+ let byTag = subSlotCache.get(slot);
10
+ if (byTag === undefined) subSlotCache.set(slot, (byTag = new Map()));
11
+ let derived = byTag.get(tag);
12
+ if (derived === undefined) {
13
+ derived = Symbol((slot.description ?? '') + ':' + tag);
14
+ byTag.set(tag, derived);
15
+ }
16
+ return derived;
17
+ }
@@ -0,0 +1,2 @@
1
+ export { useSortable, type UseSortableInput } from './useSortable';
2
+ export { isSortable, isSortableOperation } from '@dnd-kit/dom/sortable';
@@ -0,0 +1,195 @@
1
+ import { useCallback } from 'octane';
2
+ import type { Data } from '@dnd-kit/abstract';
3
+ import { Sortable, defaultSortableTransition } from '@dnd-kit/dom/sortable';
4
+ import type { SortableInput } from '@dnd-kit/dom/sortable';
5
+ import { batch, deepEqual } from '@dnd-kit/state';
6
+ import { useInstance } from '../core/hooks/useInstance';
7
+ import { subSlot } from '../internal';
8
+ import { useDeepSignal } from '../hooks/useDeepSignal';
9
+ import { useImmediateEffect } from '../hooks/useImmediateEffect';
10
+ import { useIsomorphicLayoutEffect } from '../hooks/useIsomorphicLayoutEffect';
11
+ import { useOnElementChange } from '../hooks/useOnElementChange';
12
+ import { useOnValueChange } from '../hooks/useOnValueChange';
13
+ import { currentValue, type RefOrValue } from '../utilities/currentValue';
14
+
15
+ export interface UseSortableInput<T extends Data = Data> extends Omit<
16
+ SortableInput<T>,
17
+ 'handle' | 'element' | 'target'
18
+ > {
19
+ handle?: RefOrValue<Element>;
20
+ element?: RefOrValue<Element>;
21
+ target?: RefOrValue<Element>;
22
+ }
23
+
24
+ export function useSortable<T extends Data = Data>(input: UseSortableInput<T>, slot?: symbol) {
25
+ const {
26
+ accept,
27
+ collisionDetector,
28
+ collisionPriority,
29
+ id,
30
+ data,
31
+ element,
32
+ handle,
33
+ index,
34
+ group,
35
+ disabled,
36
+ modifiers,
37
+ sensors,
38
+ target,
39
+ type,
40
+ plugins,
41
+ } = input;
42
+ const transition = { ...defaultSortableTransition, ...input.transition };
43
+ const sortable = useInstance(
44
+ (manager) =>
45
+ new Sortable(
46
+ {
47
+ ...input,
48
+ transition,
49
+ register: false,
50
+ handle: currentValue(handle),
51
+ element: currentValue(element),
52
+ target: currentValue(target),
53
+ },
54
+ manager,
55
+ ),
56
+ subSlot(slot, 'instance'),
57
+ );
58
+ const tracked = useDeepSignal(sortable, shouldUpdateSynchronously, subSlot(slot, 'signal'));
59
+
60
+ useOnValueChange(id, () => (sortable.id = id), subSlot(slot, 'id'));
61
+ useIsomorphicLayoutEffect(
62
+ () => {
63
+ batch(() => {
64
+ sortable.group = group;
65
+ sortable.index = index;
66
+ });
67
+ },
68
+ [sortable, group, index],
69
+ subSlot(slot, 'position'),
70
+ );
71
+ useOnValueChange(type, () => (sortable.type = type), subSlot(slot, 'type'));
72
+ useOnValueChange(
73
+ accept,
74
+ () => (sortable.accept = accept),
75
+ undefined,
76
+ deepEqual,
77
+ subSlot(slot, 'accept'),
78
+ );
79
+ useOnValueChange(data, () => data && (sortable.data = data), subSlot(slot, 'data'));
80
+ useOnValueChange(
81
+ index,
82
+ () => {
83
+ if (sortable.manager?.dragOperation.status.idle && transition.idle) {
84
+ sortable.refreshShape();
85
+ }
86
+ },
87
+ useImmediateEffect,
88
+ undefined,
89
+ subSlot(slot, 'refresh'),
90
+ );
91
+ useOnElementChange(handle, (value) => (sortable.handle = value), subSlot(slot, 'handle'));
92
+ useOnElementChange(element, (value) => (sortable.element = value), subSlot(slot, 'element'));
93
+ useOnElementChange(target, (value) => (sortable.target = value), subSlot(slot, 'target'));
94
+ useOnValueChange(
95
+ disabled,
96
+ () => (sortable.disabled = disabled ?? false),
97
+ undefined,
98
+ deepEqual,
99
+ subSlot(slot, 'disabled'),
100
+ );
101
+ useOnValueChange(
102
+ sensors,
103
+ () => (sortable.sensors = sensors),
104
+ undefined,
105
+ deepEqual,
106
+ subSlot(slot, 'sensors'),
107
+ );
108
+ useOnValueChange(
109
+ collisionDetector,
110
+ () => (sortable.collisionDetector = collisionDetector),
111
+ subSlot(slot, 'collision'),
112
+ );
113
+ useOnValueChange(
114
+ collisionPriority,
115
+ () => (sortable.collisionPriority = collisionPriority),
116
+ subSlot(slot, 'priority'),
117
+ );
118
+ useOnValueChange(
119
+ plugins,
120
+ () => (sortable.plugins = plugins),
121
+ undefined,
122
+ deepEqual,
123
+ subSlot(slot, 'plugins'),
124
+ );
125
+ useOnValueChange(
126
+ transition,
127
+ () => (sortable.transition = transition),
128
+ undefined,
129
+ deepEqual,
130
+ subSlot(slot, 'transition'),
131
+ );
132
+ useOnValueChange(
133
+ modifiers,
134
+ () => (sortable.modifiers = modifiers),
135
+ undefined,
136
+ deepEqual,
137
+ subSlot(slot, 'modifiers'),
138
+ );
139
+ useOnValueChange(
140
+ input.alignment,
141
+ () => (sortable.alignment = input.alignment),
142
+ subSlot(slot, 'alignment'),
143
+ );
144
+
145
+ const keepDuringDrag = (value: Element | null, current: Element | undefined) =>
146
+ !value && current?.isConnected && !sortable.manager?.dragOperation.status.idle;
147
+
148
+ return {
149
+ sortable: tracked,
150
+ get isDragging() {
151
+ return tracked.isDragging;
152
+ },
153
+ get isDropping() {
154
+ return tracked.isDropping;
155
+ },
156
+ get isDragSource() {
157
+ return tracked.isDragSource;
158
+ },
159
+ get isDropTarget() {
160
+ return tracked.isDropTarget;
161
+ },
162
+ handleRef: useCallback(
163
+ (value: Element | null) => {
164
+ sortable.handle = value ?? undefined;
165
+ },
166
+ [sortable],
167
+ subSlot(slot, 'handle-ref'),
168
+ ),
169
+ ref: useCallback(
170
+ (value: Element | null) => {
171
+ if (!keepDuringDrag(value, sortable.element)) sortable.element = value ?? undefined;
172
+ },
173
+ [sortable],
174
+ subSlot(slot, 'ref'),
175
+ ),
176
+ sourceRef: useCallback(
177
+ (value: Element | null) => {
178
+ if (!keepDuringDrag(value, sortable.source)) sortable.source = value ?? undefined;
179
+ },
180
+ [sortable],
181
+ subSlot(slot, 'source-ref'),
182
+ ),
183
+ targetRef: useCallback(
184
+ (value: Element | null) => {
185
+ if (!keepDuringDrag(value, sortable.target)) sortable.target = value ?? undefined;
186
+ },
187
+ [sortable],
188
+ subSlot(slot, 'target-ref'),
189
+ ),
190
+ };
191
+ }
192
+
193
+ function shouldUpdateSynchronously(key: string, oldValue: any, newValue: any): boolean {
194
+ return key === 'isDragSource' && !newValue && oldValue;
195
+ }
@@ -0,0 +1,13 @@
1
+ export type Ref<T> = { current: T | null | undefined };
2
+
3
+ export type RefOrValue<T> = T | Ref<T> | null | undefined;
4
+
5
+ function isRef<T>(value: RefOrValue<T>): value is Ref<T> {
6
+ return value != null && typeof value === 'object' && 'current' in value;
7
+ }
8
+
9
+ export function currentValue<T>(value: RefOrValue<T>): NonNullable<T> | undefined {
10
+ if (value == null) return undefined;
11
+ if (isRef(value)) return value.current ?? undefined;
12
+ return value as NonNullable<T>;
13
+ }
@@ -0,0 +1 @@
1
+ export { currentValue, type Ref, type RefOrValue } from './currentValue';