npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ptahjs/dnd

v0.0.3

Published

A lightweight, framework-agnostic drag-and-drop library built on the Pointer Events API. Designed with a plugin-first architecture for building canvas editors, sortable lists, and other interactive UIs.

Readme

@ptahjs/dnd

A lightweight, framework-agnostic drag-and-drop library built on the Pointer Events API. Designed with a plugin-first architecture for building canvas editors, sortable lists, and other interactive UIs.

Live Demo →

Features

  • Pointer Events based — unified mouse, touch, and stylus support
  • Plugin architecture — all behaviors (mirror, drop feedback, auto-scroll, transform) are optional plugins
  • RAF-driven rendering — per-frame measure → compute → commit pipeline prevents layout thrashing
  • Namespace support — isolate independent drag-and-drop zones within the same page
  • Transform controller — drag-scope mode with built-in move, resize, and rotate for canvas editors
  • Auto-scroll — edge-triggered scrolling during drag
  • Zero dependencies — no runtime dependencies

Installation

npm install @ptahjs/dnd

Quick Start

import { Dnd, MirrorService, DropService } from '@ptahjs/dnd'
import '@ptahjs/dnd/style'

const dnd = new Dnd({ root: '#container' })
dnd.use(new MirrorService())
dnd.use(new DropService())

dnd.on('dragstart', (payload) => console.log('drag started', payload))
dnd.on('drop', (payload) => console.log('dropped', payload))
dnd.on('cancel', (payload) => console.log('cancelled', payload))

HTML Markup

Use data attributes to declare draggable elements and drop targets:

<!-- Root container -->
<div id="container">

  <!-- Drop target -->
  <div drop>
    <!-- Draggable element -->
    <div drag data-data='{"id":1,"type":"card"}'>Drag me</div>
  </div>

  <!-- Combined draggable + drop target -->
  <div dragdrop data-namespace="list-a">Item</div>

</div>

| Attribute | Description | |---|---| | drag | Marks an element as draggable | | drop | Marks an element as a drop target | | dragdrop | Element is both draggable and a drop target | | drag-handle | Restricts drag to a specific handle element inside the draggable | | data-namespace | Groups draggable/drop elements — only elements in the same namespace interact | | data-data | JSON data payload attached to the draggable | | copy | Creates a copy on drop instead of moving | | drag-scope | Defines a canvas-style scope for TransformControllerService | | drop-indicator | Enables directional drop indicator (top, right, bottom, left, or all) | | ignore-mirror | Dragging this element does not create a mirror clone | | ignore-click | Clicks on this element do not affect active selection | | resizable / rotatable | Per-element toggle for resize/rotate handles (defaults to true within drag-scope) | | scale-ratio | Canvas zoom ratio applied to pointer coordinates in drag-scope mode |

API

new Dnd(config?)

Creates a DnD instance.

| Option | Type | Default | Description | |---|---|---|---| | root | HTMLElement \| string | — | Root container element or CSS selector | | threshold | number | 3 | Pixel distance before drag is considered started |

Instance methods

// Register a plugin
dnd.use(plugin)

// Set or replace the root container
dnd.setRoot(element)

// Override drop permission (called every frame)
dnd.canDrop = (payload) => payload.data.type !== 'locked'

// Custom mirror rendering
dnd.renderMirror = (ctx) => {
  const el = document.createElement('div')
  el.textContent = 'Custom mirror'
  return el
}

// Event listeners
dnd.on('dragstart', handler)
dnd.on('drag', handler)
dnd.on('drop', handler)
dnd.on('cancel', handler)
dnd.off('drop', handler)

// Clean up everything
dnd.destroy()

dnd.monitor

The current drag session object. Read-only during an active drag.

| Property | Description | |---|---| | active | Whether a drag session is active (pointer is down) | | started | Whether the drag threshold has been exceeded | | x, y | Current pointer coordinates | | dx, dy | Delta from drag start | | sourceEl | The element being dragged | | handleEl | The handle element that was grabbed | | currentDrop | The drop target currently under the pointer | | currentAllowed | Whether canDrop returned true for the current target | | currentDropRect | Bounding rect of the current drop target | | indicatorRegion | Active drop indicator direction (top / right / bottom / left) | | data | Parsed data from the draggable's data-data attribute | | namespace | Namespace of the drag session | | isCopy | Whether the draggable has the copy attribute |

Events

All event handlers receive a payload object.

dnd.on('dragstart', ({ source, data, namespace }) => { /* ... */ })
dnd.on('drag',      ({ source, currentDrop, currentAllowed, x, y }) => { /* ... */ })
dnd.on('drop',      ({ source, currentDrop, indicatorRegion, data }) => { /* ... */ })
dnd.on('cancel',    ({ source, data }) => { /* ... */ })

Built-in Plugins (Services)

All services are optional. Register them with dnd.use(new ServiceName()).

MirrorService

Creates a floating clone of the dragged element that follows the pointer. Adds dnd-dragging class to the source element during drag.

dnd.use(new MirrorService())

The clone can be customized via dnd.renderMirror. To disable the mirror for a specific element, add the ignore-mirror attribute to it.

DropService

Maintains currentDrop, currentDropRect, and currentAllowed on the session. Applies dnd-canDrop or dnd-noDrop CSS class to the active drop target.

dnd.use(new DropService())

DropIndicatorService

Shows a directional insertion indicator (top/right/bottom/left) on the active drop target. Requires the drop target to have a drop-indicator attribute.

dnd.use(new DropIndicatorService())
<!-- Enable all four directions -->
<div drop drop-indicator>...</div>

<!-- Enable specific directions only -->
<div drop drop-indicator="top bottom">...</div>

The indicator element has class dnd-indicator and toggles dnd-indicator--top, dnd-indicator--right, dnd-indicator--bottom, dnd-indicator--left direction classes.

AutoScrollService

Automatically scrolls the nearest scrollable ancestor (or window) when the pointer approaches the edge of the container during drag.

dnd.use(new AutoScrollService({
  edge: 48,       // Edge trigger distance in px (default: 48)
  minSpeed: 180,  // Minimum scroll speed in px/s (default: 180)
  maxSpeed: 600,  // Maximum scroll speed in px/s (default: 600)
  allowWindowScroll: true  // Allow scrolling window (default: true)
}))

ActiveSelectionService

Tracks the selected (active) draggable per namespace. Adds dnd-active class to the selected element. Clicking outside clears the selection.

Within a drag-scope container, selecting an element injects resize/rotate handles into it.

dnd.use(new ActiveSelectionService())

TransformControllerService

Enables move, resize, and rotate within a drag-scope container. Designed for canvas editors with elements that use CSS transform for positioning.

dnd.use(new TransformControllerService({
  resizable: true,
  rotatable: true,
  boundary: true,         // Constrain movement within drag-scope
  scaleRatio: 1,          // Canvas zoom ratio (can also be set via data-scale-ratio)
  snapToGrid: false,
  gridX: 10,
  gridY: 10,
  aspectRatio: undefined, // Lock aspect ratio during resize
  minWidth: 10,
  minHeight: 10,
  maxWidth: 0,            // 0 = no limit
  maxHeight: 0,
  rotateSnap: false,
  rotateStep: 15,         // Snap angle in degrees
  snap: true,             // Snap-to-element alignment
  snapThreshold: 10,      // Snap threshold in px
  markline: true,         // Show alignment guide lines
}))

Emitted events (via dnd.on):

| Event | Description | |---|---| | draggable:drag | Element moved — payload includes el, x, y, width, height, angle | | draggable:resize | Element resized — same payload | | draggable:rotate | Element rotated — same payload | | draggable:drop | Drag ended with a committed transform — includes final x, y, width, height, angle |

HTML markup for drag-scope:

<div drag-scope data-scale-ratio="1">
  <div drag resizable rotatable
       style="transform: translate(100px, 50px); width: 200px; height: 120px;">
    Canvas element
  </div>
</div>

CSS Classes

| Class | Applied to | Description | |---|---|---| | dnd-dragging | Source element | While drag is active | | dnd-mirror | Mirror clone | The floating drag ghost | | dnd-active | Selected draggable | While element is selected | | dnd-canDrop | Drop target | When canDrop returns true | | dnd-noDrop | Drop target | When canDrop returns false | | dnd-indicator | Indicator element | The drop direction indicator | | dnd-indicator-active | Indicator element | When indicator is visible | | dnd-indicator--top/right/bottom/left | Indicator element | Active direction |

Writing a Custom Plugin

A plugin is a plain object or class instance with optional lifecycle hooks. Register it with dnd.use(plugin).

const myPlugin = {
  order: 50, // lower = earlier execution

  onAttach(dnd) {
    // Called once when plugin is registered
  },

  onRootChange(nextRoot, prevRoot, signal) {
    // Called when dnd.setRoot() is called
    nextRoot.addEventListener('contextmenu', handler, { signal })
  },

  onDown(ctx, event) {
    // Pointer down — drag not yet started
  },

  onStart(ctx) {
    // Drag threshold exceeded, drag is now active
  },

  onMeasure(ctx) {
    // Read-only DOM measurements (called every frame)
  },

  onCompute(ctx) {
    // Pure calculations based on measurements (no DOM writes)
  },

  onCommit(ctx) {
    // Write DOM changes via ctx.frame to batch updates
    ctx.frame.toggleClass(element, 'my-class', true)
    ctx.frame.setStyle(element, 'opacity', '0.5')
  },

  onAfterDrag(ctx) {
    // Post-commit hook, e.g. for auto-scroll
    // Return true or { scrolled: true } to request a re-render next frame
  },

  onEnd(ctx, meta) {
    // meta.ended: true = drop, false = cancel
    // meta.reason: 'pointerup' | 'blur' | 'destroy'
  },

  onDestroy(dnd, session) {
    // Cleanup when dnd.destroy() is called
  }
}

dnd.use(myPlugin)

Context object (ctx)

| Property | Description | |---|---| | ctx.session | The current drag session (monitor) | | ctx.store | Cross-session store (selectedByNs map) | | ctx.frame | Frame command queue for batched DOM writes | | ctx.adapter | DOM adapter for measurements and hit testing | | ctx.dnd | The Dnd instance | | ctx.payload(type) | Builds an event payload for the given event type |

Architecture

Dnd (Facade)
├── PointerSensor      — captures pointerdown/move/up events
├── State              — finite state machine (DOWN → MOVE → END)
├── DomAdapter         — DOM queries and per-frame rect caching
├── RafScheduler       — requestAnimationFrame loop (only runs when dirty)
├── FrameContext       — batched DOM command queue for the current frame
└── PluginRuntime      — ordered plugin lifecycle dispatcher
    ├── MirrorService
    ├── DropService
    ├── DropIndicatorService
    ├── AutoScrollService
    ├── ActiveSelectionService
    └── TransformControllerService

Per-frame pipeline (while dragging):

pointermove → State.dispatch(MOVE) → scheduler.request()
                                           ↓
                                      requestAnimationFrame
                                           ↓
                               PluginRuntime.onMeasure   (read DOM)
                               PluginRuntime.onCompute   (pure math)
                               emit('drag', payload)
                               PluginRuntime.onCommit    (write DOM)
                               FrameContext.commit()
                               PluginRuntime.onAfterDrag (scroll, etc.)

License

MIT