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

@pyreon/dnd

v0.23.0

Published

Signal-driven drag and drop for Pyreon — wraps @atlaskit/pragmatic-drag-and-drop

Readme

@pyreon/dnd

Signal-driven drag and drop over @atlaskit/pragmatic-drag-and-drop.

A small Pyreon-native wrapper over Atlassian's pragmatic-drag-and-drop (the same library Trello / Jira ship with). pdnd handles the native-event lifecycle, hit-testing, and edge detection; @pyreon/dnd adapts every state field into a Pyreon signal (isDragging / isOver / activeId / overEdge / dragData) so consumers compose with effect / computed / JSX without re-bridging. Five hooks cover the common surfaces — single draggable, single drop target, sortable list with edge detection + auto-scroll + keyboard reordering, native-file drop with MIME / count filtering, and a global drag monitor for overlays / analytics.

Install

bun add @pyreon/dnd @pyreon/core @pyreon/reactivity
# pragmatic-drag-and-drop is a runtime dependency, installed automatically

Quick start — single draggable + drop target

import { useDraggable, useDroppable } from '@pyreon/dnd'

function Card(props: { card: { id: string; title: string } }) {
  let el: HTMLElement | null = null
  const { isDragging } = useDraggable({
    element: () => el,
    data: { id: props.card.id, type: 'card' },
  })

  return (
    <div ref={(node) => (el = node)} class={() => (isDragging() ? 'opacity-50' : '')}>
      {props.card.title}
    </div>
  )
}

function DropZone() {
  let el: HTMLElement | null = null
  const { isOver } = useDroppable({
    element: () => el,
    canDrop: (data) => data.type === 'card',
    onDrop: (data) => acceptCard(data.id as string),
  })

  return (
    <div ref={(node) => (el = node)} class={() => (isOver() ? 'bg-blue-50' : '')}>
      Drop here
    </div>
  )
}

Hooks

useDraggable({ element, data, handle?, disabled?, onDragStart?, onDragEnd? })

Make an element draggable. data may be an object OR a function for dynamic payloads. disabled is reactive (accepts a function). handle lets you scope drag initiation to a sub-element.

type Result = { isDragging: () => boolean }

useDroppable({ element, data?, canDrop?, onDragEnter?, onDragLeave?, onDrop? })

Make an element a drop target. canDrop(sourceData) filters; return false to reject. data is attached to the drop event so handlers can read target metadata.

type Result = { isOver: () => boolean }

useSortable({ items, by, onReorder, axis? })

Full sortable list with edge detection, auto-scroll, and keyboard reordering. by matches Pyreon's <For by={...}> pattern so the same key extractor flows through.

const cols = signal<Column[]>([])

const { containerRef, itemRef, activeId, overId, overEdge } = useSortable({
  items: () => cols(),
  by: (c) => c.id,
  onReorder: (next) => cols.set(next),
  axis: 'vertical', // or 'horizontal'
})

;<ul ref={containerRef}>
  <For each={cols()} by={(c) => c.id}>
    {(col) => (
      <li
        ref={itemRef(col.id)}
        class={() => (activeId() === col.id ? 'dragging' : '')}
        style={() =>
          overId() === col.id && overEdge() === 'top'
            ? 'border-top: 2px solid blue'
            : ''
        }
      >
        {col.name}
      </li>
    )}
  </For>
</ul>

Behaviour:

  • Auto-scroll when dragging near container edges
  • overEdge signal — 'top'/'bottom' (vertical) or 'left'/'right' (horizontal)
  • Keyboard reordering with Alt+Arrow keys
  • ARIA: role="listitem", aria-roledescription, tabindex

useFileDrop({ element, onDrop, accept?, maxFiles?, disabled? })

Native file-drop zone. accept mirrors <input accept> syntax (['image/*', '.pdf']); maxFiles enforces an upper bound; both filter the array passed to onDrop.

type Result = {
  isOver: () => boolean // files dragged over THIS zone
  isDraggingFiles: () => boolean // files dragged anywhere on the page
}

isDraggingFiles is useful for showing a "drop here" affordance the moment files enter the window — not just when they hover the specific zone.

useDragMonitor({ canMonitor?, onDragStart?, onDrop? })

Page-global drag state — for overlays, analytics, or coordinating multiple drag-and-drop areas.

type Result = {
  isDragging: () => boolean
  dragData: () => DragData | null
}
const { isDragging, dragData } = useDragMonitor({
  canMonitor: (data) => data.type === 'card',
  onDrop: (source, target) => track('reorder', { source, target }),
})

;<Show when={isDragging()}>
  <div class="global-drag-overlay">Dragging: {() => dragData()?.name}</div>
</Show>

Types

type DragData = Record<string, unknown>
type DropEdge = 'top' | 'bottom' | 'left' | 'right'
type DropLocation = { edge: DropEdge | null; data: DragData }

Common patterns

Cross-list sortable (kanban columns)

Multiple useSortable instances pointing at different column signals — combine with useDragMonitor for cross-list logic.

Disable while saving

useDraggable({
  element: () => el,
  data: { id },
  disabled: () => isSaving(), // reactive — re-evaluates on signal change
})

Dynamic data

useDraggable({
  element: () => el,
  data: () => ({ id: item.id(), position: position() }),
})

Gotchas

  • Hooks are SSR-safe — they return zero-state accessors when document is undefined. Real registration happens at first browser tick.
  • element: () => el must return the SAME element across reads until the component unmounts. Reassigning el to a new node mid-life re-registers but stale closures from onDragStart callbacks fire against the OLD node.
  • useSortable requires items to be reactive (a getter or signal call) — the hook needs to re-derive on insert / remove. Passing a captured array snapshot breaks reordering.
  • canMonitor / canDrop run on every drag event — keep them cheap. For expensive checks, derive a flag in a computed upstream.
  • useFileDrop only fires on REAL file drags from the OS — not from useDraggable (those go through pdnd's element adapter). The two adapters are isolated.
  • onDrop receives accepted files only — files rejected by accept / maxFiles are silently filtered. Pair with onDragEnter / isOver if you need user feedback on rejection.
  • @pyreon/dnd does NOT bundle pdnd — the pragmatic-drag-and-drop chunks come from your app's bundle graph. ~6KB minified for the element adapter (the common case).

Documentation

Full docs: docs.pyreon.dev/docs/dnd (or docs/docs/dnd.md in this repo).

License

MIT