# How to Implement Drag-and-Drop Functionality in LunaTV with @dnd-kit

> Learn how to implement drag-and-drop functionality in LunaTV using @dnd-kit for smooth vertical sorting of video sources and categories on desktop and mobile devices.

- Repository: [MoonTechLab/LunaTV](https://github.com/MoonTechLab/LunaTV)
- Tags: how-to-guide
- Published: 2026-09-08

---

**LunaTV implements drag-and-drop reordering in its admin interface using @dnd-kit's sensor configuration, `DndContext` wrapper, and `useSortable` hook to enable smooth vertical sorting of video sources and categories across desktop and mobile devices.**

LunaTV is an open-source streaming platform that provides administrators with intuitive content management tools. The admin dashboard leverages the **@dnd-kit** library to deliver native drag-and-drop interactions for organizing video sources, custom categories, and live channels. This implementation separates sensor detection, drag state management, and sortable row rendering into distinct architectural layers within the Next.js application.

## Installing the Required Dependencies

The drag-and-drop system relies on four official @dnd-kit packages defined in [`package.json`](https://github.com/MoonTechLab/LunaTV/blob/main/package.json) (lines 22–25). These provide the core drag engine, sorting logic, movement modifiers, and CSS utilities.

- `@dnd-kit/core` – Provides the `DndContext`, sensors, and collision detection algorithms.
- `@dnd-kit/sortable` – Supplies `SortableContext`, `useSortable`, and sorting strategies.
- `@dnd-kit/modifiers` – Includes `restrictToVerticalAxis` and `restrictToParentElement` for constrained movement.
- `@dnd-kit/utilities` – Exports the `CSS` helper for transform string conversions.

Install them using your package manager:

```bash
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/modifiers @dnd-kit/utilities

```

## Configuring Sensors for Cross-Device Support

LunaTV supports both mouse and touch interactions by combining `PointerSensor` and `TouchSensor` within the `useSensors` hook. According to the source code in [`src/app/admin/page.tsx`](https://github.com/MoonTechLab/LunaTV/blob/main/src/app/admin/page.tsx) (lines 5–12), this configuration ensures the UI responds correctly on desktop while preventing accidental drags on mobile.

The `PointerSensor` activates after a 5-pixel movement, while the `TouchSensor` requires a 250ms delay with 5-pixel tolerance to distinguish scrolling from dragging.

```tsx
import {
  PointerSensor,
  TouchSensor,
  useSensor,
  useSensors,
} from '@dnd-kit/core';

const sensors = useSensors(
  useSensor(PointerSensor, {
    activationConstraint: { distance: 5 },
  }),
  useSensor(TouchSensor, {
    activationConstraint: { delay: 250, tolerance: 5 },
  })
);

```

## Wrapping the List in DndContext

The list of sources or categories is wrapped in a `DndContext` component instantiated in [`src/app/admin/page.tsx`](https://github.com/MoonTechLab/LunaTV/blob/main/src/app/admin/page.tsx) (lines 998–1005). This provider receives the sensors, collision detection strategy, drag-end handler, and modifiers that constrain movement.

The `closestCenter` collision detection algorithm ensures dropped items snap to the nearest target. The `restrictToVerticalAxis` and `restrictToParentElement` modifiers prevent horizontal movement and keep elements within the table boundaries.

```tsx
import { DndContext, closestCenter } from '@dnd-kit/core';
import { restrictToParentElement, restrictToVerticalAxis } from '@dnd-kit/modifiers';

<DndContext
  sensors={sensors}
  collisionDetection={closestCenter}
  onDragEnd={handleDragEnd}
  modifiers={[restrictToVerticalAxis, restrictToParentElement]}
>
  {/* Sortable children */}
</DndContext>

```

## Creating Sortable Rows with useSortable

Individual table rows become draggable through the `DraggableRow` component defined in [`src/app/admin/page.tsx`](https://github.com/MoonTechLab/LunaTV/blob/main/src/app/admin/page.tsx) (lines 2326–2338). This component uses the `useSortable` hook, which returns essential properties: `setNodeRef` for the DOM element, `transform` and `transition` for CSS animation, and `attributes` and `listeners` for accessibility and event handling.

The `CSS.Transform.toString()` utility converts the transform object into a valid CSS value. Setting `touchAction: 'none'` on the grip handle prevents browser scrolling while dragging on touch devices.

```tsx
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { GripVertical } from 'lucide-react';

function DraggableRow({ item }: { item: { id: string; name: string } }) {
  const { attributes, listeners, setNodeRef, transform, transition } =
    useSortable({ id: item.id });

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
  } as React.CSSProperties;

  return (
    <tr ref={setNodeRef} style={style} className="select-none">
      <td
        className="cursor-grab"
        style={{ touchAction: 'none' }}
        {...attributes}
        {...listeners}
      >
        <GripVertical size={16} />
      </td>
      <td>{item.name}</td>
    </tr>
  );
}

```

## Managing Sort Order with SortableContext

Inside the `DndContext`, a `SortableContext` (lines 1010–1014 in [`src/app/admin/page.tsx`](https://github.com/MoonTechLab/LunaTV/blob/main/src/app/admin/page.tsx)) manages the array order and recycling of sortable items. It requires a stable array of unique IDs derived from the data—such as `sources.map(s => s.key)` or `` categories.map(c => `${c.query}:${c.type}`) ``—and a sorting strategy.

The `verticalListSortingStrategy` calculates positions for items stacked in a single column, ensuring smooth rearrangement within table bodies.

```tsx
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';

<SortableContext
  items={items.map((i) => i.id)}
  strategy={verticalListSortingStrategy}
>
  <tbody>
    {items.map((item) => (
      <DraggableRow key={item.id} item={item} />
    ))}
  </tbody>
</SortableContext>

```

## Handling Drag Events and Persisting Order

The `handleDragEnd` function (implemented in [`src/app/admin/page.tsx`](https://github.com/MoonTechLab/LunaTV/blob/main/src/app/admin/page.tsx), lines 998–1005) processes the drag completion event. It extracts `active` (source) and `over` (destination) IDs, calculates new indices using `arrayMove`, and updates local React state immediately for visual feedback.

When the `orderChanged` flag is set, a "Save Order" button appears (lines 1230–1234). Clicking it invokes `handleSaveOrder`, which posts the reordered array to `/api/admin/source` to persist the configuration server-side.

```tsx
function handleDragEnd(event: any) {
  const { active, over } = event;
  
  if (active.id !== over?.id) {
    const oldIndex = items.findIndex((i) => i.id === active.id);
    const newIndex = items.findIndex((i) => i.id === over?.id);
    const reordered = arrayMove(items, oldIndex, newIndex);
    
    setItems(reordered);
    
    fetch('/api/admin/source', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        action: 'reorder',
        order: reordered.map((i) => i.id),
      }),
    });
  }
}

```

## Summary

- **Sensor configuration** in [`src/app/admin/page.tsx`](https://github.com/MoonTechLab/LunaTV/blob/main/src/app/admin/page.tsx) combines `PointerSensor` and `TouchSensor` to support desktop and mobile interactions with distinct activation constraints.
- **DndContext** wraps the entire list, applying `closestCenter` collision detection and modifiers that restrict movement to the vertical axis and parent boundaries.
- **useSortable** hook transforms each table row into a draggable element by providing refs, CSS transforms, and event listeners.
- **SortableContext** manages item order using `verticalListSortingStrategy` and requires a stable array of unique identifiers.
- **Persistence** occurs via `handleDragEnd` for optimistic UI updates and `handleSaveOrder` for POST requests to `/api/admin/source`.

## Frequently Asked Questions

### What sensors does LunaTV use for drag-and-drop interactions?

LunaTV configures both `PointerSensor` and `TouchSensor` using the `useSensors` hook in [`src/app/admin/page.tsx`](https://github.com/MoonTechLab/LunaTV/blob/main/src/app/admin/page.tsx). The pointer sensor activates after a 5-pixel drag distance, while the touch sensor requires a 250-millisecond press with 5-pixel tolerance to differentiate dragging from scrolling.

### How does LunaTV restrict drag movement to vertical only?

The implementation passes the `restrictToVerticalAxis` modifier from `@dnd-kit/modifiers` to the `DndContext` component. Additionally, `restrictToParentElement` prevents draggable items from leaving the table container, ensuring the UI remains stable during reordering.

### Where is the drag-and-drop logic implemented in the LunaTV codebase?

All drag-and-drop logic resides in [`src/app/admin/page.tsx`](https://github.com/MoonTechLab/LunaTV/blob/main/src/app/admin/page.tsx). This file contains the sensor setup (lines 5–12), the `DndContext` and `handleDragEnd` definitions (lines 998–1005), the `SortableContext` configuration (lines 1010–1014), and the `DraggableRow` component (lines 2326–2338).

### How does LunaTV persist reordered items to the backend?

When a drag operation completes, the `handleDragEnd` function updates local state optimistically. If the order changes, a "Save Order" button appears; clicking it triggers `handleSaveOrder`, which sends a POST request to `/api/admin/source` with the new item order array, ensuring the configuration survives page reloads.