> ## Documentation Index
> Fetch the complete documentation index at: https://docs.yoopta.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Block Drag & Drop

> Drag and drop support for reordering blocks in the editor

## Overview

The Block DnD components provide drag-and-drop functionality for reordering blocks in your editor. Built on top of [@dnd-kit](https://dndkit.com/), it integrates seamlessly with the `renderBlock` API to keep DnD logic outside the core editor.

<Frame>
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/kin/images/block-dnd.gif" alt="Block Drag and Drop in action" />
</Frame>

## Features

* **@dnd-kit powered** — industry-standard drag and drop library
* **Multi-block selection** — drag multiple selected blocks at once
* **Visual feedback** — drop indicators and drag overlays
* **Accessible** — keyboard support built-in
* **Customizable** — style drag handles and drop indicators
* **Non-invasive** — uses `renderBlock` API, no core editor changes

## Installation

```bash theme={null}
npm install @yoopta/ui
# or
yarn add @yoopta/ui
```

The `@yoopta/ui` package includes `@dnd-kit` as a dependency.

## Quick Start

```tsx theme={null}
import { useMemo, useCallback } from 'react';
import { createYooptaEditor, YooptaEditor, type RenderBlockProps } from '@yoopta/editor';
import { BlockDndContext, SortableBlock, DragHandle } from '@yoopta/ui/block-dnd';

function MyEditor() {
  const editor = useMemo(
    () => createYooptaEditor({ plugins, marks }),
    [],
  );

  const renderBlock = useCallback(({ children, blockId }: RenderBlockProps) => (
    <SortableBlock id={blockId} useDragHandle>
      <DragHandle blockId={blockId}>
        <GripVertical size={16} />
      </DragHandle>
      {children}
    </SortableBlock>
  ), []);

  return (
    <BlockDndContext editor={editor}>
      <YooptaEditor
        editor={editor}
        onChange={(value) => console.log(value)}
        placeholder="Type / to open menu..."
        renderBlock={renderBlock}
      >
        {/* Other UI components */}
      </YooptaEditor>
    </BlockDndContext>
  );
}
```

## How It Works

The Block DnD system consists of three main parts:

1. **BlockDndContext** — Wraps the editor, provides DnD context and handles block reordering
2. **SortableBlock** — Wrapper for each block that makes it sortable. When using `DragHandle`, set `useDragHandle={true}` to prevent the entire block from being draggable
3. **DragHandle** — The draggable trigger element. Must be used with `SortableBlock` that has `useDragHandle={true}`

The `renderBlock` prop on `YooptaEditor` allows you to wrap each block with DnD components without modifying the core editor.

**Important**: When using `DragHandle`, you must set `useDragHandle={true}` on `SortableBlock`. This ensures that drag listeners are only applied to the handle, not the entire block.

## API Reference

### `BlockDndContext`

The root provider that wraps your editor and handles drag events.

```tsx theme={null}
<BlockDndContext
  editor={editor}
  onDragStart={(event) => console.log('Drag started', event)}
  onDragEnd={(event) => console.log('Drag ended', event)}
>
  <YooptaEditor ... />
</BlockDndContext>
```

**Props:**

| Prop          | Type                              | Description                 |
| ------------- | --------------------------------- | --------------------------- |
| `editor`      | `YooEditor`                       | The Yoopta editor instance  |
| `children`    | `ReactNode`                       | Editor and other components |
| `onDragStart` | `(event: DragStartEvent) => void` | Called when drag starts     |
| `onDragEnd`   | `(event: DragEndEvent) => void`   | Called when drag ends       |

### `SortableBlock`

Wrapper component that makes a block sortable.

```tsx theme={null}
<SortableBlock
  id={blockId}
  disabled={isReadOnly}
  className="my-sortable-block"
>
  {children}
</SortableBlock>
```

**Props:**

| Prop            | Type        | Description                                                                                                     |
| --------------- | ----------- | --------------------------------------------------------------------------------------------------------------- |
| `id`            | `string`    | Block ID (required)                                                                                             |
| `children`      | `ReactNode` | Block content                                                                                                   |
| `disabled`      | `boolean`   | Disable sorting for this block                                                                                  |
| `className`     | `string`    | Custom CSS classes                                                                                              |
| `useDragHandle` | `boolean`   | If `true`, listeners won't be applied to the block (use `DragHandle` instead). Required when using `DragHandle` |

### `DragHandle`

The draggable trigger component. Users drag this to move blocks.

```tsx theme={null}
<DragHandle
  blockId={blockId}
  className="my-drag-handle"
>
  <GripVertical size={16} />
</DragHandle>
```

**Props:**

| Prop        | Type                      | Description                                                                                                             |
| ----------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `blockId`   | `string \| null`          | Block ID this handle controls                                                                                           |
| `children`  | `ReactNode`               | Handle content (usually an icon)                                                                                        |
| `className` | `string`                  | Custom CSS classes                                                                                                      |
| `onClick`   | `(e: MouseEvent) => void` | Called when handle is clicked (not dragged)                                                                             |
| `asChild`   | `boolean`                 | If `true`, merges props and event handlers with the child element (useful for integrating with other button components) |

### `useBlockDnd`

Hook for accessing DnD state and utilities.

```tsx theme={null}
const {
  activeId,
  activeBlock,
  isDragging,
  draggedIds,
  getOrderedBlockIds,
} = useBlockDnd({ editor });
```

**Returns:**

| Property      | Type                      | Description                                    |
| ------------- | ------------------------- | ---------------------------------------------- |
| `activeId`    | `string \| null`          | Currently dragged block ID                     |
| `activeBlock` | `YooptaBlockData \| null` | Currently dragged block data                   |
| `isDragging`  | `boolean`                 | Whether a drag is in progress                  |
| `draggedIds`  | `string[]`                | All block IDs being dragged (for multi-select) |

### `useBlockDndContext`

Hook to access the DnD context value directly.

```tsx theme={null}
const { activeId, activeBlock, isDragging, draggedIds, editor } = useBlockDndContext();
```

### `getOrderedBlockIds`

Utility function to get block IDs sorted by their order.

```tsx theme={null}
import { getOrderedBlockIds } from '@yoopta/ui/block-dnd';

const orderedIds = getOrderedBlockIds(editor);
// ['block-1', 'block-2', 'block-3', ...]
```

## Examples

### Basic Setup

```tsx theme={null}
import { useMemo, useCallback } from 'react';
import { createYooptaEditor, YooptaEditor, type RenderBlockProps } from '@yoopta/editor';
import { BlockDndContext, SortableBlock, DragHandle } from '@yoopta/ui/block-dnd';
import { GripVertical } from 'lucide-react';

function Editor() {
  const editor = useMemo(() => createYooptaEditor({
    plugins: PLUGINS,
    marks: MARKS,
  }), []);

  const renderBlock = useCallback(({ children, blockId }: RenderBlockProps) => (
    <SortableBlock id={blockId} useDragHandle>
      <div className="flex items-start gap-2">
        <DragHandle blockId={blockId} className="opacity-0 group-hover:opacity-100">
          <GripVertical size={16} className="text-gray-400" />
        </DragHandle>
        <div className="flex-1">{children}</div>
      </div>
    </SortableBlock>
  ), []);

  return (
    <BlockDndContext editor={editor}>
      <YooptaEditor
        editor={editor}
        onChange={(value) => console.log(value)}
        placeholder="Type / to open menu..."
        renderBlock={renderBlock}
      />
    </BlockDndContext>
  );
}
```

### With FloatingBlockActions

Integrate DnD with the existing FloatingBlockActions component:

```tsx theme={null}
import { FloatingBlockActions } from '@yoopta/ui/floating-block-actions';
import { BlockDndContext, SortableBlock, DragHandle } from '@yoopta/ui/block-dnd';

function Editor() {
  const editor = useMemo(
    () => createYooptaEditor({ plugins, marks }),
    [],
  );

  const renderBlock = useCallback(({ children, blockId }: RenderBlockProps) => (
    <SortableBlock id={blockId} useDragHandle>
      {children}
    </SortableBlock>
  ), []);

  return (
    <BlockDndContext editor={editor}>
      <YooptaEditor editor={editor} onChange={onChange} placeholder="Type / to open menu..." renderBlock={renderBlock}>
        <FloatingBlockActions>
          {({ blockId }) => (
            <>
              <FloatingBlockActions.Button onClick={() => onPlusClick(blockId)}>
                <PlusIcon />
              </FloatingBlockActions.Button>
              {/* Use DragHandle with asChild to integrate with FloatingBlockActions.Button */}
              <DragHandle blockId={blockId} asChild>
                <FloatingBlockActions.Button title="Drag to reorder">
                  <GripVertical size={16} />
                </FloatingBlockActions.Button>
              </DragHandle>
            </>
          )}
        </FloatingBlockActions>
      </YooptaEditor>
    </BlockDndContext>
  );
}
```

### Multi-Block Drag

When blocks are selected (via Shift+Click or SelectionBox), dragging one will move all selected blocks:

```tsx theme={null}
function Editor() {
  const editor = useMemo(
    () => createYooptaEditor({ plugins, marks }),
    [],
  );

  const renderBlock = useCallback(({ children, blockId, block }: RenderBlockProps) => {
    // Check if this block is in the multi-selection
    const selectedPaths = editor.path.selected;
    const isSelected = selectedPaths?.includes(block.meta.order);

    return (
      <SortableBlock id={blockId} useDragHandle>
        <div className={isSelected ? 'ring-2 ring-blue-500' : ''}>
          <DragHandle blockId={blockId}>
            <GripVertical size={16} />
          </DragHandle>
          {children}
        </div>
      </SortableBlock>
    );
  }, [editor]);

  return (
    <BlockDndContext editor={editor}>
      <YooptaEditor editor={editor} onChange={onChange} renderBlock={renderBlock}>
        <SelectionBox />
      </YooptaEditor>
    </BlockDndContext>
  );
}
```

### Read-Only Mode

Disable DnD in read-only mode:

```tsx theme={null}
function Editor({ readOnly }) {
  const editor = useMemo(
    () => createYooptaEditor({ plugins, marks, readOnly }),
    [readOnly],
  );

  const renderBlock = useCallback(({ children, blockId }: RenderBlockProps) => (
    <SortableBlock id={blockId} disabled={readOnly} useDragHandle={!readOnly}>
      {!readOnly && (
        <DragHandle blockId={blockId}>
          <GripVertical size={16} />
        </DragHandle>
      )}
      {children}
    </SortableBlock>
  ), [readOnly]);

  return (
    <BlockDndContext editor={editor}>
      <YooptaEditor editor={editor} onChange={onChange} renderBlock={renderBlock} />
    </BlockDndContext>
  );
}
```

## Styling

### CSS Variables

```css theme={null}
:root {
  /* Drag handle */
  --yoopta-block-dnd-handle-opacity: 0;
  --yoopta-block-dnd-handle-hover-opacity: 1;
  --yoopta-block-dnd-handle-color: var(--yoopta-ui-muted-foreground);
  --yoopta-block-dnd-focus-ring: #3b82f6;

  /* Drop indicator */
  --yoopta-block-dnd-indicator-color: #3b82f6;
  --yoopta-block-dnd-indicator-height: 2px;

  /* Dragging state */
  --yoopta-block-dnd-dragging-opacity: 0.5;
  --yoopta-block-dnd-dragging-bg: var(--yoopta-ui-accent);

  /* Drag overlay */
  --yoopta-block-dnd-overlay-bg: #ffffff;
  --yoopta-block-dnd-overlay-border: #e5e7eb;
  --yoopta-block-dnd-overlay-text: #374151;
  --yoopta-block-dnd-overlay-text-accent: #1f2937;
  --yoopta-block-dnd-overlay-icon-color: #6b7280;
}
```

### Custom Styles

```tsx theme={null}
<SortableBlock id={blockId} className="group relative" useDragHandle>
  <DragHandle
    blockId={blockId}
    className="absolute -left-8 top-1 opacity-0 group-hover:opacity-100 transition-opacity"
  >
    <GripVertical className="w-4 h-4 text-gray-400 hover:text-gray-600" />
  </DragHandle>
  {children}
</SortableBlock>
```

## The `renderBlock` API

The `renderBlock` prop on `YooptaEditor` is the key to integrating DnD without modifying the core editor.

```tsx theme={null}
export type RenderBlockProps = {
  /** The block data */
  block: YooptaBlockData;
  /** The rendered block content */
  children: ReactNode;
  /** The block's unique ID */
  blockId: string;
  /** The block's order index */
  index: number;
};

<YooptaEditor
  editor={editor}
  onChange={onChange}
  placeholder="Type / to open menu..."
  renderBlock={({ children, blockId }: RenderBlockProps) => (
    <SortableBlock id={blockId}>
      {children}
    </SortableBlock>
  )}
/>
```

This pattern allows you to:

* Add DnD support
* Add custom block wrappers
* Implement block-level features without forking the editor

## Best Practices

<AccordionGroup>
  <Accordion title="Memoize renderBlock">
    Always wrap `renderBlock` in `useCallback` to prevent unnecessary re-renders:

    ```tsx theme={null}
    const renderBlock = useCallback(({ children, blockId }: RenderBlockProps) => (
      <SortableBlock id={blockId} useDragHandle>{children}</SortableBlock>
    ), []);
    ```
  </Accordion>

  <Accordion title="Place BlockDndContext outside YooptaEditor">
    The context must wrap the entire editor:

    ```tsx theme={null}
    // Correct
    <BlockDndContext editor={editor}>
      <YooptaEditor editor={editor} renderBlock={renderBlock}>
        {/* UI components */}
      </YooptaEditor>
    </BlockDndContext>

    // Wrong - context inside editor
    <YooptaEditor editor={editor}>
      <BlockDndContext editor={editor}>
        {/* ... */}
      </BlockDndContext>
    </YooptaEditor>
    ```
  </Accordion>

  <Accordion title="Handle read-only mode">
    Disable drag handles and sorting in read-only mode:

    ```tsx theme={null}
    const isReadOnly = useYooptaReadOnly();

    <SortableBlock id={blockId} disabled={isReadOnly} useDragHandle={!isReadOnly}>
      {!isReadOnly && <DragHandle blockId={blockId}>...</DragHandle>}
      {children}
    </SortableBlock>
    ```
  </Accordion>
</AccordionGroup>

## TypeScript

Full type definitions are exported:

```tsx theme={null}
import type {
  BlockDndContextProps,
  BlockDndContextValue,
  SortableBlockData,
  SortableBlockProps,
  DragHandleProps,
  DropIndicatorProps,
  UseBlockDndOptions,
  UseBlockDndReturn,
} from '@yoopta/ui/block-dnd';
```

## Related Components

<CardGroup cols={2}>
  <Card title="FloatingBlockActions" icon="hand-pointer" href="/ui/floating-block-actions">
    Combine with floating actions for a complete block UI
  </Card>

  <Card title="SelectionBox" icon="object-group" href="/ui/selection-box">
    Multi-select blocks for batch operations
  </Card>
</CardGroup>
