Drag-to-Reorder List
moumenlab/drag-to-reorder-listFreeComponent
A reorderable list where siblings glide one slot and the drop plays a FLIP, keyboard included.
Live preview
live · rendered by the registry
Rendered by moumenlab on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://lab.moumen.dev/r/drag-to-reorder-list.jsonSource
1"use client";23import { useEffect, useLayoutEffect, useRef, useState } from "react";4import { MotionConfig, animate, motion, useReducedMotion, type MotionValue, motionValue } from "motion/react";56// Drag-to-reorder list with FLIP.7//8// Three motion systems working together without fighting, all driven through9// motion/react's motion values:10//11// 1. The dragged card follows the pointer RAW - its y motion value is12// `jump()`ed straight from the pointermove handler (no easing: any easing13// between hand and card reads as lag; no React render on the hot path).14// 2. Siblings glide out of the way: as the dragged card crosses slot15// boundaries each displaced sibling's y is `animate()`d exactly one slot -16// retargetable, so reversing mid-glide just works.17// 3. The drop is a FLIP: capture First rects before the commit, let React18// reflow, then Invert each card back to its old screen pixels and19// `animate()` it home. Nothing jumps.20//21// Plus rubber-banding past the ends (4:1 damped), pointer capture, multi-touch22// protection, and a full keyboard path (grip → Space grabs → arrows move →23// Escape cancels) narrated to screen readers. Requires the lab-theme tokens.24// Fully Tailwind, no CSS files.2526const DRAG_THRESHOLD = 4;27const EASE = [0.22, 1, 0.36, 1] as const;28const MOVE = { duration: 0.2, ease: EASE } as const;29const LIFT = { duration: 0.16, ease: EASE } as const;3031export interface ReorderItem {32 id: string;33 label: string;34 meta?: string;35}3637export interface ReorderState {38 order: string[];39 dragging: string | null;40 from: number | null;41 to: number | null;42 grabbed: boolean;43 lastMove: { label: string; from: number; to: number } | null;44}4546interface DragData {47 row: HTMLLIElement;48 pointerId: number;49 index: number;50 startY: number;51 active: boolean;52 slot: number;53 to: number;54 rows?: HTMLLIElement[];55}5657export default function DragReorderList({58 items: controlledItems,59 defaultItems = DEFAULT_ITEMS,60 onReorder,61 flip = true,62 inspect = false,63 onStateChange,64}: {65 /** Controlled item order; pair with onReorder. Omit for uncontrolled. */66 items?: ReorderItem[];67 defaultItems?: ReorderItem[];68 onReorder?: (items: ReorderItem[]) => void;69 /** false = hard snap, no glides/FLIP. */70 flip?: boolean;71 inspect?: boolean;72 onStateChange?: (state: ReorderState) => void;73}) {74 const [uncontrolled, setUncontrolled] = useState(controlledItems ?? defaultItems);75// … truncated
What it pulls in
npm packages
Other registry items
Files it writes
More from moumenlab
All 14 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Caret-Anchored Mention Popovercaret-mention-popover | moumenlab | Components | Free | 1 dep | |
| Command Palette with Argument Chipscommand-palette | moumenlab | Components | Free | 1 dep | |
| File Upload Staging Areafile-upload-staging | moumenlab | Components | Free | 1 dep | |
| Hover-Expand Icon Striphover-expand-icon-strip | moumenlab | Components | Free | 1 dep | |
| Inertial Wheel Listinertial-wheel-list | moumenlab | Components | Free | 1 dep | |
| Morphing Checkout Flowmorphing-checkout | moumenlab | Components | Free | 1 dep | |
| OTP Segmented Inputotp-segmented-input | moumenlab | Components | Free | 1 dep | |
| Schedule Builderschedule-builder | moumenlab | Components | Free | 1 dep |
