pointrix 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +971 -0
  3. package/dist/pointrix-drag.cjs +1 -0
  4. package/dist/pointrix-drag.d.cts +189 -0
  5. package/dist/pointrix-drag.d.mts +189 -0
  6. package/dist/pointrix-drag.mjs +2 -0
  7. package/dist/pointrix-dropzone.cjs +1 -0
  8. package/dist/pointrix-dropzone.d.cts +99 -0
  9. package/dist/pointrix-dropzone.d.mts +99 -0
  10. package/dist/pointrix-dropzone.mjs +2 -0
  11. package/dist/pointrix-gesture.cjs +1 -0
  12. package/dist/pointrix-gesture.d.cts +119 -0
  13. package/dist/pointrix-gesture.d.mts +119 -0
  14. package/dist/pointrix-gesture.mjs +2 -0
  15. package/dist/pointrix-modifiers.cjs +1 -0
  16. package/dist/pointrix-modifiers.d.cts +334 -0
  17. package/dist/pointrix-modifiers.d.mts +334 -0
  18. package/dist/pointrix-modifiers.mjs +2 -0
  19. package/dist/pointrix-nano.cjs +1 -0
  20. package/dist/pointrix-nano.d.cts +82 -0
  21. package/dist/pointrix-nano.d.mts +82 -0
  22. package/dist/pointrix-nano.mjs +2 -0
  23. package/dist/pointrix-react.cjs +1 -0
  24. package/dist/pointrix-react.d.cts +537 -0
  25. package/dist/pointrix-react.d.mts +537 -0
  26. package/dist/pointrix-react.mjs +2 -0
  27. package/dist/pointrix-resize.cjs +1 -0
  28. package/dist/pointrix-resize.d.cts +193 -0
  29. package/dist/pointrix-resize.d.mts +193 -0
  30. package/dist/pointrix-resize.mjs +2 -0
  31. package/dist/pointrix-sortable.cjs +1 -0
  32. package/dist/pointrix-sortable.d.cts +89 -0
  33. package/dist/pointrix-sortable.d.mts +89 -0
  34. package/dist/pointrix-sortable.mjs +2 -0
  35. package/dist/pointrix-vue.cjs +1 -0
  36. package/dist/pointrix-vue.d.cts +485 -0
  37. package/dist/pointrix-vue.d.mts +485 -0
  38. package/dist/pointrix-vue.mjs +2 -0
  39. package/dist/pointrix.cjs +1 -0
  40. package/dist/pointrix.d.cts +784 -0
  41. package/dist/pointrix.d.mts +784 -0
  42. package/dist/pointrix.mjs +2 -0
  43. package/package.json +144 -0
package/README.md ADDED
@@ -0,0 +1,971 @@
1
+ # Hyperact
2
+
3
+ Ultra-fast, zero-dependency drag/resize/gesture library for modern browsers. A high-performance, tree-shakeable alternative to interact.js.
4
+
5
+ ## Features
6
+
7
+ - **Tiny footprint** -- core is 1.6 KB gzipped; full bundle under 10 KB gzipped
8
+ - **Zero runtime dependencies** -- nothing to audit, nothing to break
9
+ - **Modular architecture** -- import only drag, resize, gesture, dropzone, or sortable
10
+ - **Modifier pipeline** -- composable restrict, snap, inertia, magnetic snap, rubberband, and auto-scroll modifiers
11
+ - **Unified pointer events** -- mouse, touch, and pen handled identically
12
+ - **RAF-batched updates** -- single shared `requestAnimationFrame` loop across all instances
13
+ - **GPU-accelerated** -- uses `translate3d` for all transforms
14
+ - **Framework integrations** -- first-class React hooks/components and Vue 3 composables/directives
15
+ - **TypeScript-first** -- strict types for every option, event, and return value
16
+ - **Tree-shakeable** -- ES module sub-path exports; bundlers drop what you don't use
17
+ - **Event listener API** -- chainable `.on()` / `.off()` for all interaction events
18
+ - **Batch creation** -- `interactAll()` to create instances for many elements at once
19
+ - **Advanced filtering** -- `allowFrom` / `ignoreFrom` selectors, mouse button filtering, hold delay
20
+ - **Tap and gesture detection** -- built-in tap, double-tap, and hold callbacks
21
+ - **ARIA accessibility** -- automatic ARIA attributes for draggable, sortable, and dropzone elements with live screen reader announcements
22
+ - **i18n / Localization** -- all screen reader strings are customizable via `setMessages()` for any language
23
+
24
+ ## Bundle Sizes
25
+
26
+ Measured from the `dist/` output (minified with terser, gzipped):
27
+
28
+ | Import path | Min | Gzip | What you get |
29
+ |---|---|---|---|
30
+ | `pointrix/nano` | 5.4 KB | 1.6 KB | Core pointer tracking, velocity, tap detection |
31
+ | `pointrix/drag` | 11.9 KB | 3.3 KB | Draggable with axis, bounds, grid, momentum, modifiers |
32
+ | `pointrix/resize` | 11.5 KB | 3.2 KB | Resizable with edges, aspect ratio, min/max, modifiers |
33
+ | `pointrix/gesture` | 8.3 KB | 2.3 KB | Multi-touch pinch, rotate, pan |
34
+ | `pointrix/dropzone` | 3.0 KB | 1.0 KB | Drop targets with overlap modes |
35
+ | `pointrix/sortable` | 18.2 KB | 5.0 KB | Sortable lists with cross-container group support |
36
+ | `pointrix/modifiers` | 8.3 KB | 2.6 KB | All modifiers (restrict, snapGrid, snapTargets, magneticSnap, inertia, autoScroll) |
37
+ | `pointrix` | 38.6 KB | 9.8 KB | Full bundle with everything |
38
+
39
+ For comparison, interact.js ships approximately 140 KB minified.
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ npm install pointrix
45
+ # or
46
+ pnpm add pointrix
47
+ # or
48
+ yarn add pointrix
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ```ts
54
+ import { draggable } from 'pointrix/drag'
55
+
56
+ const drag = draggable('#my-element', {
57
+ onDragMove: (e) => console.log(e.totalX, e.totalY),
58
+ })
59
+
60
+ // Later:
61
+ drag.destroy()
62
+ ```
63
+
64
+ Every factory function accepts an `HTMLElement` or a CSS selector string. Every instance has a `.destroy()` method and an `.enabled` property.
65
+
66
+ ---
67
+
68
+ ## API Reference
69
+
70
+ ### Core Options (HyperactOptions)
71
+
72
+ All interaction types (draggable, resizable, gesturable) inherit these base options:
73
+
74
+ | Option | Type | Default | Description |
75
+ |---|---|---|---|
76
+ | `threshold` | `number` | `3` | Pixels of movement before interaction starts |
77
+ | `preventScroll` | `boolean` | `true` | Prevent touch scrolling while interacting |
78
+ | `holdDelay` | `number` | `0` | Hold delay in ms -- pointer must be held this long before the interaction starts |
79
+ | `mouseButtons` | `number` | `0` (any) | Bitmask of allowed mouse buttons: `1` = left, `2` = right, `4` = middle |
80
+ | `allowFrom` | `string` | -- | CSS selector -- only start if the pointer target matches |
81
+ | `ignoreFrom` | `string` | -- | CSS selector -- never start if the pointer target matches |
82
+ | `touchAction` | `string` | `'none'` | CSS `touch-action` value applied to the element |
83
+ | `styleCursor` | `boolean` | `true` | Whether to set cursor styles automatically |
84
+ | `enabled` | `boolean` | `true` | Enable or disable the instance (also available as a property) |
85
+ | `onTap` | `(event) => void` | -- | Called on tap (pointer down + up without exceeding threshold) |
86
+ | `onDoubleTap` | `(event) => void` | -- | Called on double-tap (two taps within 300 ms on the same target) |
87
+ | `onHold` | `(event) => void` | -- | Called when the pointer is held still for `holdDuration` ms without starting an interaction |
88
+ | `holdDuration` | `number` | `600` | Time in ms before `onHold` fires |
89
+
90
+ ---
91
+
92
+ ### Event Listener API
93
+
94
+ Every instance (Hyperact, Draggable, Resizable, Gesturable) exposes chainable `.on()` and `.off()` methods for subscribing to events imperatively. This is an alternative to passing callbacks in the options object.
95
+
96
+ ```ts
97
+ const drag = draggable('#el')
98
+
99
+ drag.on('dragstart', (e) => console.log('started'))
100
+ drag.on('dragmove', (e) => console.log(e.totalX, e.totalY))
101
+ drag.off('dragmove', handler)
102
+ ```
103
+
104
+ You can also read whether an interaction is currently in progress with the `.interacting` property:
105
+
106
+ ```ts
107
+ if (drag.interacting) {
108
+ console.log('drag is active')
109
+ }
110
+ ```
111
+
112
+ #### Events by class
113
+
114
+ **Hyperact (nano)**
115
+
116
+ | Event | Description |
117
+ |---|---|
118
+ | `start` | Interaction started (threshold exceeded or hold delay elapsed) |
119
+ | `move` | Pointer moved during an active interaction |
120
+ | `end` | Interaction ended |
121
+ | `tap` | Pointer released without exceeding threshold |
122
+
123
+ **Draggable**
124
+
125
+ | Event | Description |
126
+ |---|---|
127
+ | `dragstart` | Drag started |
128
+ | `dragmove` | Pointer moved during drag |
129
+ | `dragend` | Drag ended |
130
+
131
+ **Resizable**
132
+
133
+ | Event | Description |
134
+ |---|---|
135
+ | `resizestart` | Resize started |
136
+ | `resizemove` | Size changed during resize |
137
+ | `resizeend` | Resize ended |
138
+
139
+ **Gesturable**
140
+
141
+ | Event | Description |
142
+ |---|---|
143
+ | `gesturestart` | Gesture started |
144
+ | `gesturemove` | Gesture updated |
145
+ | `gestureend` | Gesture ended |
146
+
147
+ ---
148
+
149
+ ### Draggable
150
+
151
+ ```ts
152
+ import { draggable } from 'pointrix/drag'
153
+ ```
154
+
155
+ #### Options
156
+
157
+ | Option | Type | Default | Description |
158
+ |---|---|---|---|
159
+ | `axis` | `'x' \| 'y' \| 'xy' \| 'start'` | `'xy'` | Constrain movement to one axis. `'start'` auto-detects the axis from the initial movement direction |
160
+ | `startAxis` | `'x' \| 'y'` | -- | Only start the drag if the initial movement direction matches this axis |
161
+ | `handle` | `string \| HTMLElement` | -- | Only start drag when pointer is inside this element/selector |
162
+ | `bounds` | `'parent' \| HTMLElement \| {left?, top?, right?, bottom?}` | -- | Restrict movement within a region |
163
+ | `grid` | `{x: number, y: number}` | -- | Snap position to a grid |
164
+ | `momentum` | `boolean \| {friction?: number, minSpeed?: number}` | `false` | Physics-based momentum after release |
165
+ | `droppable` | `boolean` | `false` | Integrate with the `Dropzone` system |
166
+ | `modifiers` | `Modifier[]` | -- | Modifier chain applied each frame |
167
+ | `cursorChecker` | `(action: 'idle' \| 'grab' \| 'grabbing') => string` | -- | Custom function to determine the cursor for each drag state |
168
+ | `threshold` | `number` | `3` | Pixels of movement before drag starts |
169
+ | `preventScroll` | `boolean` | `true` | Prevent touch scrolling while dragging |
170
+
171
+ #### Events
172
+
173
+ | Callback | Event type | Key fields |
174
+ |---|---|---|
175
+ | `onDragStart` | `DragEvent` | `dx`, `dy`, `totalX`, `totalY`, `velocityX`, `velocityY` |
176
+ | `onDragMove` | `DragEvent` | Same as above, updated each frame |
177
+ | `onDragEnd` | `DragEvent` | Final values |
178
+
179
+ #### Methods
180
+
181
+ ```ts
182
+ const d = draggable(el, options)
183
+ d.setPosition(100, 200) // Jump to a transform position
184
+ d.getPosition() // { x: number, y: number }
185
+ d.enabled = false // Disable (cancels active drag)
186
+ d.destroy() // Remove all listeners
187
+ ```
188
+
189
+ #### Example
190
+
191
+ ```ts
192
+ import { draggable } from 'pointrix/drag'
193
+ import { snapGrid, inertia } from 'pointrix/modifiers'
194
+
195
+ draggable('#card', {
196
+ bounds: 'parent',
197
+ momentum: { friction: 0.92 },
198
+ modifiers: [
199
+ snapGrid({ x: 20, y: 20 }),
200
+ inertia({ resistance: 8 }),
201
+ ],
202
+ onDragEnd: (e) => console.log('Dropped at', e.totalX, e.totalY),
203
+ })
204
+ ```
205
+
206
+ #### Example -- axis auto-detection
207
+
208
+ ```ts
209
+ draggable('#swipeable', {
210
+ axis: 'start', // locks to x or y based on initial swipe direction
211
+ onDragEnd: (e) => console.log(e.totalX, e.totalY),
212
+ })
213
+ ```
214
+
215
+ ---
216
+
217
+ ### Resizable
218
+
219
+ ```ts
220
+ import { resizable } from 'pointrix/resize'
221
+ ```
222
+
223
+ #### Options
224
+
225
+ | Option | Type | Default | Description |
226
+ |---|---|---|---|
227
+ | `edges` | `{top?, right?, bottom?, left?}` (booleans) | All `true` | Which edges/corners can be dragged |
228
+ | `handleSize` | `number` | `10` | Pixel width of the resize handle area |
229
+ | `minWidth` | `number` | `50` | Minimum width in px |
230
+ | `minHeight` | `number` | `50` | Minimum height in px |
231
+ | `maxWidth` | `number` | `Infinity` | Maximum width in px |
232
+ | `maxHeight` | `number` | `Infinity` | Maximum height in px |
233
+ | `aspectRatio` | `number \| 'preserve'` | -- | Lock aspect ratio (number or preserve current) |
234
+ | `square` | `boolean` | `false` | Shorthand for `aspectRatio: 1` |
235
+ | `invert` | `'none' \| 'negate' \| 'reposition'` | `'none'` | How to handle resizing past the opposite edge. `'negate'` allows negative sizes; `'reposition'` flips the element |
236
+ | `grid` | `{width: number, height: number}` | -- | Snap size to a grid |
237
+ | `modifiers` | `Modifier[]` | -- | Modifier chain |
238
+ | `cursorChecker` | `(edge: string \| null) => string` | -- | Custom function to determine the cursor for each edge |
239
+
240
+ #### Events
241
+
242
+ | Callback | Event type | Key fields |
243
+ |---|---|---|
244
+ | `onResizeStart` | `ResizeEvent` | `width`, `height`, `deltaWidth`, `deltaHeight`, `edges` |
245
+ | `onResizeMove` | `ResizeEvent` | Same, updated each frame |
246
+ | `onResizeEnd` | `ResizeEvent` | Final values |
247
+
248
+ #### Methods
249
+
250
+ ```ts
251
+ const r = resizable(el, options)
252
+ r.setSize(300, 200)
253
+ r.getSize() // { width: number, height: number }
254
+ r.enabled = false
255
+ r.destroy()
256
+ ```
257
+
258
+ #### Example
259
+
260
+ ```ts
261
+ resizable('#panel', {
262
+ edges: { right: true, bottom: true },
263
+ minWidth: 200,
264
+ aspectRatio: 16 / 9,
265
+ onResizeMove: (e) => console.log(`${e.width}x${e.height}`),
266
+ })
267
+ ```
268
+
269
+ #### Example -- invert mode
270
+
271
+ ```ts
272
+ resizable('#box', {
273
+ invert: 'reposition', // allows dragging past opposite edge
274
+ square: true, // maintain 1:1 aspect ratio
275
+ onResizeMove: (e) => console.log(e.width, e.height),
276
+ })
277
+ ```
278
+
279
+ ---
280
+
281
+ ### Gesturable
282
+
283
+ ```ts
284
+ import { gesturable } from 'pointrix/gesture'
285
+ ```
286
+
287
+ Multi-touch gesture recognition (pinch-to-zoom, rotate). Activates when the required number of pointers are down.
288
+
289
+ #### Options
290
+
291
+ | Option | Type | Default | Description |
292
+ |---|---|---|---|
293
+ | `minPointers` | `number` | `2` | Pointer count required to activate |
294
+
295
+ #### Events
296
+
297
+ | Callback | Event type | Key fields |
298
+ |---|---|---|
299
+ | `onGestureStart` | `GestureEvent` | `scale`, `rotation`, `distance`, `angle`, `center`, `deltaScale`, `deltaAngle` |
300
+ | `onGestureMove` | `GestureEvent` | Same, updated each frame |
301
+ | `onGestureEnd` | `GestureEvent` | Final values |
302
+
303
+ #### Example
304
+
305
+ ```ts
306
+ import { gesturable } from 'pointrix/gesture'
307
+
308
+ gesturable('#canvas', {
309
+ onGestureMove: (e) => {
310
+ applyZoom(e.scale)
311
+ applyRotation(e.rotation)
312
+ },
313
+ })
314
+ ```
315
+
316
+ ---
317
+
318
+ ### Dropzone
319
+
320
+ ```ts
321
+ import { dropzone } from 'pointrix/dropzone'
322
+ ```
323
+
324
+ Define drop targets that respond to `draggable` elements created with `droppable: true`.
325
+
326
+ #### Options
327
+
328
+ | Option | Type | Default | Description |
329
+ |---|---|---|---|
330
+ | `accept` | `string \| (el: HTMLElement) => boolean` | -- | Filter which draggables can drop here |
331
+ | `overlap` | `'pointer' \| 'center' \| number` | `'pointer'` | How overlap is computed (`number` = area ratio threshold) |
332
+ | `activeClass` | `string` | -- | CSS class added while a compatible drag is in progress |
333
+ | `hoverClass` | `string` | -- | CSS class added while a draggable hovers over the zone |
334
+
335
+ #### Events
336
+
337
+ | Callback | Event type | Description |
338
+ |---|---|---|
339
+ | `onActivate` | `DropEvent` | A compatible drag started somewhere |
340
+ | `onDeactivate` | `DropEvent` | That drag ended |
341
+ | `onDragEnter` | `DropEvent` | Draggable entered this zone |
342
+ | `onDragLeave` | `DropEvent` | Draggable left this zone |
343
+ | `onDragOver` | `DropEvent` | Draggable is over this zone (fires each frame) |
344
+ | `onDrop` | `DropEvent` | Draggable was released over this zone |
345
+
346
+ `DropEvent` contains `target` (dropzone element), `draggable` (the dragged element), `overlap` (number), and `dragEvent`.
347
+
348
+ #### Example
349
+
350
+ ```ts
351
+ import { draggable } from 'pointrix/drag'
352
+ import { dropzone } from 'pointrix/dropzone'
353
+
354
+ draggable('#item', { droppable: true })
355
+
356
+ dropzone('#bin', {
357
+ accept: '.deletable',
358
+ hoverClass: 'drop-hover',
359
+ onDrop: (e) => e.draggable.remove(),
360
+ })
361
+ ```
362
+
363
+ ---
364
+
365
+ ### Sortable
366
+
367
+ ```ts
368
+ import { sortable } from 'pointrix/sortable'
369
+ ```
370
+
371
+ Drag-to-reorder lists with animated item displacement. Supports cross-container transfers via the `group` option.
372
+
373
+ #### Options
374
+
375
+ | Option | Type | Default | Description |
376
+ |---|---|---|---|
377
+ | `items` | `string` | direct children | CSS selector for sortable items |
378
+ | `axis` | `'x' \| 'y'` | `'y'` | Sort direction |
379
+ | `handle` | `string` | -- | CSS selector for drag handle within each item |
380
+ | `animationDuration` | `number` | `200` | Transition duration in ms for shifting items |
381
+ | `dragClass` | `string` | `'sortable-dragging'` | CSS class on the item being dragged |
382
+ | `hoverClass` | `string` | `'sortable-hover'` | CSS class on a container receiving a grouped item |
383
+ | `group` | `string` | -- | Group name; sortables sharing a group can exchange items |
384
+
385
+ #### Events
386
+
387
+ | Callback | Event type | Description |
388
+ |---|---|---|
389
+ | `onSort` | `SortEvent` | Order changed during drag (`item`, `oldIndex`, `newIndex`, `items`) |
390
+ | `onSortEnd` | `SortEvent` | Drag finished and DOM was reordered |
391
+ | `onAdd` | `SortTransferEvent` | An item was added from another sortable (`item`, `from`, `to`, `oldIndex`, `newIndex`) |
392
+ | `onRemove` | `SortTransferEvent` | An item was removed to another sortable |
393
+
394
+ #### Methods
395
+
396
+ ```ts
397
+ const s = sortable('#list', options)
398
+ s.getOrder() // Current item elements in order
399
+ s.move(fromIndex, toIndex) // Programmatic reorder
400
+ s.refresh() // Re-scan items (after DOM changes)
401
+ s.enabled = false
402
+ s.destroy()
403
+ ```
404
+
405
+ #### Example -- single list
406
+
407
+ ```ts
408
+ sortable('#todo-list', {
409
+ handle: '.grip',
410
+ onSortEnd: (e) => saveOrder(e.items.map(el => el.dataset.id)),
411
+ })
412
+ ```
413
+
414
+ #### Example -- cross-container groups
415
+
416
+ ```ts
417
+ sortable('#backlog', { group: 'kanban', onRemove: (e) => console.log('removed', e.item) })
418
+ sortable('#in-progress', { group: 'kanban', onAdd: (e) => console.log('added', e.item) })
419
+ sortable('#done', { group: 'kanban' })
420
+ ```
421
+
422
+ ---
423
+
424
+ ### Modifiers
425
+
426
+ ```ts
427
+ import {
428
+ restrict, snapGrid, snapTargets, magneticSnap, inertia, autoScroll,
429
+ rubberband, restrictSize, restrictEdges, snapSize, snapEdges,
430
+ } from 'pointrix/modifiers'
431
+ ```
432
+
433
+ Modifiers are composable transforms applied to the position each frame. Pass them as an array to the `modifiers` option of `draggable` or `resizable`.
434
+
435
+ #### `restrict(options)`
436
+
437
+ Clamp position within bounds.
438
+
439
+ | Option | Type | Description |
440
+ |---|---|---|
441
+ | `bounds` | `'parent' \| HTMLElement \| {left?, top?, right?, bottom?}` | Bounding region |
442
+ | `elementRect` | `{left, top, right, bottom}` (0-1 ratios) | Which part of the element must stay inside bounds |
443
+ | `endOnly` | `boolean` | Only apply restriction at drag end |
444
+
445
+ #### `snapGrid(options)`
446
+
447
+ Snap to a regular grid.
448
+
449
+ | Option | Type | Description |
450
+ |---|---|---|
451
+ | `x` | `number` | Grid cell width |
452
+ | `y` | `number` | Grid cell height |
453
+ | `offset` | `{x, y}` | Grid origin offset |
454
+ | `limits` | `{top?, left?, bottom?, right?}` | Clamp snapped position |
455
+
456
+ #### `snapTargets(options)`
457
+
458
+ Snap to arbitrary target positions with pivot support.
459
+
460
+ | Option | Type | Description |
461
+ |---|---|---|
462
+ | `targets` | `SnapTarget[]` | Array of `{x?, y?, range?}` |
463
+ | `range` | `number` (default `50`) | Default snap distance |
464
+ | `relativePoints` | `Array<PivotPreset \| {x, y}>` | Which point(s) on the element to test |
465
+ | `coordinateMode` | `'offset' \| 'parent'` | Target coordinate system |
466
+
467
+ **Pivot presets:** `'top-left'`, `'top'`, `'top-right'`, `'left'`, `'center'`, `'right'`, `'bottom-left'`, `'bottom'`, `'bottom-right'`
468
+
469
+ The returned modifier exposes `snappedTarget`, `snappedIndex`, and `isSnapped` for reading snap state.
470
+
471
+ #### `magneticSnap(options)`
472
+
473
+ Attract the element toward named targets with distance-based pull strength.
474
+
475
+ | Option | Type | Description |
476
+ |---|---|---|
477
+ | `targets` | `MagneticTarget[]` | Array of `{id, x, y, width?, height?, strength?}` |
478
+ | `distance` | `number` (default `30`) | Activation distance |
479
+ | `strength` | `number` (default `0.5`) | Pull strength (0-1) |
480
+ | `onSnap` | `(target) => void` | Called when element snaps to a target |
481
+ | `onUnsnap` | `(target) => void` | Called when element leaves a target |
482
+
483
+ Methods: `updateTargets(targets)`, `addTarget(target)`, `removeTarget(id)`, `getCurrentTarget()`, `isSnapped()`.
484
+
485
+ #### `inertia(options?)`
486
+
487
+ Continue movement after release using exponential decay.
488
+
489
+ | Option | Type | Default | Description |
490
+ |---|---|---|---|
491
+ | `resistance` | `number` | `10` | Decay constant (higher = more friction) |
492
+ | `minSpeed` | `number` | `10` | Speed below which inertia stops |
493
+ | `endSpeed` | `number` | `100` | Minimum release speed to trigger inertia |
494
+ | `smoothEnd` | `boolean` | `false` | Decelerate smoothly to current position |
495
+ | `smoothEndDuration` | `number` | `300` | Duration for smooth end (ms) |
496
+
497
+ #### `autoScroll(options?)`
498
+
499
+ Scroll a container when the pointer approaches its edges.
500
+
501
+ | Option | Type | Default | Description |
502
+ |---|---|---|---|
503
+ | `container` | `HTMLElement \| Window` | auto-detected | Scroll container |
504
+ | `speed` | `number` | `10` | Scroll speed (px/frame) |
505
+ | `margin` | `number` | `50` | Edge proximity threshold (px) |
506
+ | `acceleration` | `number` | `5` | Acceleration multiplier |
507
+
508
+ #### `rubberband(options)`
509
+
510
+ Allow the element to be dragged past bounds with elastic resistance, then snap back on release.
511
+
512
+ | Option | Type | Default | Description |
513
+ |---|---|---|---|
514
+ | `bounds` | `'parent' \| {left?, top?, right?, bottom?}` | -- | Bounding region |
515
+ | `resistance` | `number` | `0.15` | Resistance factor 0-1. Lower = more resistance |
516
+ | `maxOvershoot` | `number` | `100` | Maximum overshoot in pixels |
517
+
518
+ ```ts
519
+ draggable('#el', {
520
+ modifiers: [
521
+ rubberband({ bounds: 'parent', resistance: 0.2 }),
522
+ ],
523
+ })
524
+ ```
525
+
526
+ #### `restrictSize(options)`
527
+
528
+ Clamp the element's size during a resize interaction.
529
+
530
+ | Option | Type | Description |
531
+ |---|---|---|
532
+ | `min` | `{width?: number, height?: number}` | Minimum size |
533
+ | `max` | `{width?: number, height?: number}` | Maximum size |
534
+
535
+ ```ts
536
+ resizable('#panel', {
537
+ modifiers: [
538
+ restrictSize({ min: { width: 100, height: 100 }, max: { width: 800, height: 600 } }),
539
+ ],
540
+ })
541
+ ```
542
+
543
+ #### `restrictEdges(options)`
544
+
545
+ Restrict individual edge positions during a resize interaction.
546
+
547
+ | Option | Type | Description |
548
+ |---|---|---|
549
+ | `outer` | `{left?, top?, right?, bottom?}` | Edges cannot go beyond these values (outward limit) |
550
+ | `inner` | `{left?, top?, right?, bottom?}` | Edges cannot pass these values toward the center (inward limit) |
551
+
552
+ ```ts
553
+ resizable('#panel', {
554
+ modifiers: [
555
+ restrictEdges({
556
+ outer: { left: 0, top: 0, right: 800, bottom: 600 },
557
+ inner: { left: 100, top: 100 },
558
+ }),
559
+ ],
560
+ })
561
+ ```
562
+
563
+ #### `snapSize(options)`
564
+
565
+ Snap the element's width and height to a grid during resize.
566
+
567
+ | Option | Type | Description |
568
+ |---|---|---|
569
+ | `width` | `number` | Grid cell width for snapping |
570
+ | `height` | `number` | Grid cell height for snapping |
571
+ | `offset` | `{width?: number, height?: number}` | Grid origin offset |
572
+
573
+ ```ts
574
+ resizable('#panel', {
575
+ modifiers: [
576
+ snapSize({ width: 50, height: 50 }),
577
+ ],
578
+ })
579
+ ```
580
+
581
+ #### `snapEdges(options)`
582
+
583
+ Snap individual edges to target positions during resize.
584
+
585
+ | Option | Type | Description |
586
+ |---|---|---|
587
+ | `targets` | `SnapEdgeTarget[]` | Array of `{left?, top?, right?, bottom?, range?}` |
588
+ | `range` | `number` (default `20`) | Default snap distance |
589
+
590
+ ```ts
591
+ resizable('#panel', {
592
+ modifiers: [
593
+ snapEdges({
594
+ targets: [{ left: 0, top: 0, right: 800, bottom: 600 }],
595
+ range: 30,
596
+ }),
597
+ ],
598
+ })
599
+ ```
600
+
601
+ #### Composing modifiers
602
+
603
+ ```ts
604
+ draggable('#el', {
605
+ modifiers: [
606
+ restrict({ bounds: 'parent' }),
607
+ snapGrid({ x: 25, y: 25 }),
608
+ inertia({ resistance: 12 }),
609
+ autoScroll({ margin: 60 }),
610
+ ],
611
+ })
612
+ ```
613
+
614
+ Modifiers run in array order. Each modifier receives the output of the previous one.
615
+
616
+ ---
617
+
618
+ ### Interactable
619
+
620
+ ```ts
621
+ import { interactable } from 'pointrix'
622
+ ```
623
+
624
+ Convenience factory that creates drag, resize, and gesture instances on the same element. Hyperact coordinates them automatically (resize has priority over drag when pointer is near an edge).
625
+
626
+ ```ts
627
+ const ia = interactable('#widget', {
628
+ drag: { momentum: true },
629
+ resize: { edges: { right: true, bottom: true }, minWidth: 120 },
630
+ gesture: true,
631
+ })
632
+
633
+ ia.drag // Draggable | null
634
+ ia.resize // Resizable | null
635
+ ia.gesture // Gesturable | null
636
+ ia.destroy()
637
+ ```
638
+
639
+ Pass `true` for default options or an options object.
640
+
641
+ ---
642
+
643
+ ### interactAll()
644
+
645
+ ```ts
646
+ import { interactAll } from 'pointrix'
647
+ ```
648
+
649
+ Create interaction instances for every element matching a CSS selector. Returns an object with an `instances` array and a single `destroy()` method to tear down all of them.
650
+
651
+ ```ts
652
+ const result = interactAll('.card', { drag: true, resize: true })
653
+
654
+ result.instances // Array of interactable results
655
+ result.destroy() // Destroys all instances at once
656
+ ```
657
+
658
+ ---
659
+
660
+ ### Enabling and Disabling
661
+
662
+ Every instance has an `enabled` property. Setting it to `false` cancels any active interaction and ignores future pointer events. Setting it back to `true` re-enables the instance.
663
+
664
+ ```ts
665
+ const drag = draggable('#el', { onDragMove: (e) => console.log(e.totalX) })
666
+
667
+ // Disable
668
+ drag.enabled = false
669
+
670
+ // Re-enable
671
+ drag.enabled = true
672
+ ```
673
+
674
+ You can also check whether an interaction is in progress:
675
+
676
+ ```ts
677
+ drag.interacting // true while a drag is active
678
+ ```
679
+
680
+ ---
681
+
682
+ ## Accessibility (ARIA)
683
+
684
+ Hyperact automatically applies ARIA attributes and provides live screen reader announcements for draggable, sortable, and dropzone interactions. A visually hidden live region (`aria-live="assertive"`) and a shared instructions element are created lazily in the DOM when needed.
685
+
686
+ All ARIA behavior is enabled by default. No configuration is required for basic accessibility support.
687
+
688
+ ### Draggable ARIA
689
+
690
+ When a `draggable` is created, the following attributes are set on the element:
691
+
692
+ | Attribute | Value | Purpose |
693
+ |---|---|---|
694
+ | `tabindex` | `0` | Makes the element keyboard-focusable (only set if not already present) |
695
+ | `role` | `button` | Identifies the element as an interactive control (only set if not already present) |
696
+ | `aria-roledescription` | `draggable` | Tells screen readers this is a draggable element |
697
+ | `aria-describedby` | `grip-instructions` | Points to a visually hidden element containing keyboard instructions |
698
+ | `aria-grabbed` | `true` / `false` | Toggled when a drag starts and ends |
699
+
700
+ Screen readers will announce the element as a "draggable button" and read the keyboard instructions on focus. When a drag starts, the live region announces "Picked up"; when it ends, "Dropped".
701
+
702
+ ### Sortable ARIA
703
+
704
+ Sortable containers and their items receive additional attributes to convey list semantics and position:
705
+
706
+ **Container:**
707
+
708
+ | Attribute | Value | Purpose |
709
+ |---|---|---|
710
+ | `role` | `listbox` | Identifies the container as an ordered list (only set if not already present) |
711
+
712
+ **Items:**
713
+
714
+ | Attribute | Value | Purpose |
715
+ |---|---|---|
716
+ | `tabindex` | `0` | Keyboard-focusable |
717
+ | `role` | `option` | Identifies each item as a list option (only set if not already present) |
718
+ | `aria-roledescription` | `sortable` | Tells screen readers this is a sortable item |
719
+ | `aria-describedby` | `grip-instructions` | Keyboard instructions |
720
+ | `aria-posinset` | `1`, `2`, ... | Current position in the list (1-based) |
721
+ | `aria-setsize` | total count | Total number of items in the list |
722
+
723
+ During a sort operation, the live region announces position changes:
724
+
725
+ - **Pick up:** "Picked up [label], position 3 of 10"
726
+ - **Move:** "Moved to position 5 of 10"
727
+ - **Drop:** "Dropped [label] in position 5 of 10"
728
+
729
+ ### Dropzone ARIA
730
+
731
+ Dropzone elements receive `aria-dropeffect` to communicate their state to assistive technology:
732
+
733
+ | State | `aria-dropeffect` value |
734
+ |---|---|
735
+ | Default (initialized) | `move` |
736
+ | Active (compatible drag in progress) | `move` |
737
+ | Inactive (no compatible drag) | `none` |
738
+
739
+ The attribute is toggled automatically when a compatible draggable starts or ends.
740
+
741
+ ### Opting Out
742
+
743
+ Pass `aria: false` to any factory function to disable all ARIA attribute management for that instance:
744
+
745
+ ```ts
746
+ draggable('#el', { aria: false })
747
+ ```
748
+
749
+ ### i18n / Localization
750
+
751
+ All screen reader announcement strings can be customized using `setMessages()`. This allows full translation of every ARIA string Hyperact produces.
752
+
753
+ #### The `AriaMessages` interface
754
+
755
+ ```ts
756
+ interface AriaMessages {
757
+ instructions: string
758
+ pickedUp: (label: string, position: number, total: number) => string
759
+ movedTo: (position: number, total: number) => string
760
+ dropped: (label: string, position: number, total: number) => string
761
+ dragPickedUp: string
762
+ dragDropped: string
763
+ }
764
+ ```
765
+
766
+ #### Default values
767
+
768
+ | Key | Default |
769
+ |---|---|
770
+ | `instructions` | `'Press Space or Enter to pick up. Use arrow keys to move. Press Space or Enter to drop. Press Escape to cancel.'` |
771
+ | `pickedUp` | `` (label, pos, total) => `Picked up ${label}, position ${pos} of ${total}` `` |
772
+ | `movedTo` | `` (pos, total) => `Moved to position ${pos} of ${total}` `` |
773
+ | `dropped` | `` (label, pos, total) => `Dropped ${label} in position ${pos} of ${total}` `` |
774
+ | `dragPickedUp` | `'Picked up'` |
775
+ | `dragDropped` | `'Dropped'` |
776
+
777
+ #### Overriding messages
778
+
779
+ Use `setMessages()` with a partial or full set of replacements. Any keys you omit will keep their current values.
780
+
781
+ ```ts
782
+ import { setMessages } from 'pointrix'
783
+
784
+ setMessages({
785
+ instructions: '...',
786
+ pickedUp: (label, pos, total) => `...`,
787
+ movedTo: (pos, total) => `...`,
788
+ dropped: (label, pos, total) => `...`,
789
+ dragPickedUp: '...',
790
+ dragDropped: '...',
791
+ })
792
+ ```
793
+
794
+ #### Full translation example (German)
795
+
796
+ ```ts
797
+ import { setMessages } from 'pointrix'
798
+
799
+ setMessages({
800
+ instructions:
801
+ 'Leertaste oder Eingabetaste drücken zum Aufnehmen. Pfeiltasten zum Verschieben. Leertaste oder Eingabetaste zum Ablegen. Escape zum Abbrechen.',
802
+ pickedUp: (label, pos, total) => `${label} aufgenommen, Position ${pos} von ${total}`,
803
+ movedTo: (pos, total) => `Verschoben auf Position ${pos} von ${total}`,
804
+ dropped: (label, pos, total) => `${label} abgelegt auf Position ${pos} von ${total}`,
805
+ dragPickedUp: 'Aufgenommen',
806
+ dragDropped: 'Abgelegt',
807
+ })
808
+ ```
809
+
810
+ You can also read the current messages at any time with `getMessages()`:
811
+
812
+ ```ts
813
+ import { getMessages } from 'pointrix'
814
+
815
+ const current = getMessages()
816
+ console.log(current.instructions)
817
+ ```
818
+
819
+ ---
820
+
821
+ ## React Integration
822
+
823
+ ```ts
824
+ import { useDraggable, useResizable, useGesturable, useDropzone, useSortable, useInteractable } from 'pointrix/react'
825
+ ```
826
+
827
+ Each hook returns `{ ref, instance }`. Attach `ref` to your element; read or control the interaction through `instance.current`.
828
+
829
+ ### Hooks
830
+
831
+ ```tsx
832
+ function DraggableCard() {
833
+ const { ref } = useDraggable({
834
+ bounds: 'parent',
835
+ onDragEnd: (e) => console.log(e.totalX, e.totalY),
836
+ })
837
+
838
+ return <div ref={ref}>Drag me</div>
839
+ }
840
+ ```
841
+
842
+ Pass a dependency array as the second argument to recreate the instance when values change:
843
+
844
+ ```tsx
845
+ const { ref } = useDraggable({ axis }, [axis])
846
+ ```
847
+
848
+ ### Components
849
+
850
+ Pre-built components that forward options as props:
851
+
852
+ ```tsx
853
+ import { DraggableComponent, ResizableComponent, InteractableComponent } from 'pointrix/react'
854
+
855
+ <DraggableComponent bounds="parent" as="section" className="card">
856
+ Content
857
+ </DraggableComponent>
858
+ ```
859
+
860
+ The `as` prop controls the rendered element (default `div`).
861
+
862
+ ### Available hooks and components
863
+
864
+ | Hook | Component | Import |
865
+ |---|---|---|
866
+ | `useGrip` | `GripComponent` | `pointrix/react` |
867
+ | `useDraggable` | `DraggableComponent` | `pointrix/react` |
868
+ | `useResizable` | `ResizableComponent` | `pointrix/react` |
869
+ | `useGesturable` | `GesturableComponent` | `pointrix/react` |
870
+ | `useDropzone` | -- | `pointrix/react` |
871
+ | `useSortable` | -- | `pointrix/react` |
872
+ | `useInteractable` | `InteractableComponent` | `pointrix/react` |
873
+
874
+ ---
875
+
876
+ ## Vue 3 Integration
877
+
878
+ ```ts
879
+ import { useDraggable, vDraggable, GripPlugin } from 'pointrix/vue'
880
+ ```
881
+
882
+ ### Composables
883
+
884
+ Each composable returns `{ elRef, instance }`. Bind `elRef` with `ref=` in your template.
885
+
886
+ ```vue
887
+ <script setup lang="ts">
888
+ import { useDraggable } from 'pointrix/vue'
889
+
890
+ const { elRef } = useDraggable({
891
+ bounds: 'parent',
892
+ onDragEnd: (e) => console.log(e.totalX, e.totalY),
893
+ })
894
+ </script>
895
+
896
+ <template>
897
+ <div :ref="elRef">Drag me</div>
898
+ </template>
899
+ ```
900
+
901
+ Pass a `ref()` as options to automatically recreate the instance when options change:
902
+
903
+ ```ts
904
+ const opts = ref<DragOptions>({ axis: 'x' })
905
+ const { elRef } = useDraggable(opts)
906
+ ```
907
+
908
+ ### Directives
909
+
910
+ ```vue
911
+ <template>
912
+ <div v-draggable="{ bounds: 'parent' }">Drag me</div>
913
+ <div v-resizable="{ minWidth: 100 }">Resize me</div>
914
+ <div v-gesturable>Pinch me</div>
915
+ <div v-sortable="{ axis: 'y' }">Sort me</div>
916
+ </template>
917
+ ```
918
+
919
+ ### Plugin
920
+
921
+ Register all directives globally:
922
+
923
+ ```ts
924
+ import { createApp } from 'vue'
925
+ import { GripPlugin } from 'pointrix/vue'
926
+
927
+ createApp(App).use(GripPlugin).mount('#app')
928
+ ```
929
+
930
+ This registers `v-draggable`, `v-resizable`, `v-gesturable`, and `v-sortable`.
931
+
932
+ ### Available composables and directives
933
+
934
+ | Composable | Directive |
935
+ |---|---|
936
+ | `useGrip` | -- |
937
+ | `useDraggable` | `vDraggable` |
938
+ | `useResizable` | `vResizable` |
939
+ | `useGesturable` | `vGesturable` |
940
+ | `useDropzone` | -- |
941
+ | `useSortable` | `vSortable` |
942
+ | `useInteractable` | -- |
943
+
944
+ ---
945
+
946
+ ## Tree-Shaking and Sub-Path Imports
947
+
948
+ Every module is available as a separate entry point. Your bundler will only include the code you actually import.
949
+
950
+ ```ts
951
+ // Only drag -- pulls in nano as a dependency (~3.3 KB gzip)
952
+ import { draggable } from 'pointrix/drag'
953
+
954
+ // Only modifiers
955
+ import { snapGrid, inertia } from 'pointrix/modifiers'
956
+
957
+ // Only React hooks
958
+ import { useDraggable } from 'pointrix/react'
959
+
960
+ // Only Vue composables
961
+ import { useDraggable } from 'pointrix/vue'
962
+
963
+ // Full bundle if you need everything
964
+ import { interactable, interactAll, draggable, resizable, gesturable, dropzone, sortable } from 'pointrix'
965
+ ```
966
+
967
+ All entry points ship ESM (`.mjs`) and CJS (`.cjs`) with full TypeScript declarations.
968
+
969
+ ## License
970
+
971
+ MIT