import { DemoBox } from "../../src/components/demo/demo-box";

# Customizability

The Drag and Drop Feature provides several options to customize the behavior of the drag-and-drop interaction,
including several ways to restrict what and when users can drag and drop items.

## No reordering

Set the config option `canReorder` to false, to disable users from being able to choose arbitrary drop locations
within a specific item. Drag lines will not be rendered, and can be omitted from the render implementation.
Dragging an item between several items will either always target the specific item that is hovered over as new parent,
or the parent of the item that is currently being hovered over if the direct hover target is not a folder.

The [Drop event described earlier](https://headless-tree.lukasbach.com/llm/dnd/overview.md) will always contain only an item as target,
never `childIndex`, `insertionIndex`, `dragLineIndex` and `dragLineLevel`.

```ts jsx
import type { Meta } from "@storybook/react";
import React, { useState } from "react";
import {
  TreeState,
  dragAndDropFeature,
  hotkeysCoreFeature,
  keyboardDragAndDropFeature,
  selectionFeature,
  syncDataLoaderFeature,
} from "@headless-tree/core";
import { AssistiveTreeDescription, useTree } from "@headless-tree/react";
import cn from "classnames";

const meta = {
  title: "React/Drag and Drop/Cannot Drop Inbetween",
  tags: ["feature/dnd"],
} satisfies Meta;

export default meta;

// story-start
export const CannotDropInbetween = () => {
  const [state, setState] = useState<Partial<TreeState<any>>>({
    expandedItems: ["root-1", "root-1-2"],
  });
  const tree = useTree<string>({
    state,
    setState,
    rootItemId: "root",
    getItemName: (item) => item.getItemData(),
    isItemFolder: (item) => item.getItemMeta().level < 2,
    canReorder: false,
    onDrop: (items, target) => {
      alert(
        `Dropped ${items.map((item) =>
          item.getId(),
        )} on ${target.item.getId()}, ${JSON.stringify(target)}`,
      );
    },
    indent: 20,
    dataLoader: {
      getItem: (itemId) => itemId,
      getChildren: (itemId) => [
        `${itemId}-1`,
        `${itemId}-2`,
        `${itemId}-3`,
        `${itemId}-4`,
        `${itemId}-5`,
        `${itemId}-6`,
      ],
    },
    features: [
      syncDataLoaderFeature,
      selectionFeature,
      hotkeysCoreFeature,
      dragAndDropFeature,
      keyboardDragAndDropFeature,
    ],
  });

  return (
    <div {...tree.getContainerProps()} className="tree">
      <AssistiveTreeDescription tree={tree} />
      {tree.getItems().map((item) => (
        <button
          {...item.getProps()}
          key={item.getId()}
          style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }}
        >
          <div
            className={cn("treeitem", {
              focused: item.isFocused(),
              expanded: item.isExpanded(),
              selected: item.isSelected(),
              folder: item.isFolder(),
              drop: item.isDragTarget(),
            })}
          >
            {item.getItemName()}
          </div>
        </button>
      ))}
      <div style={tree.getDragLineStyle()} className="dragline" />
    </div>
  );
};
```

## Limiting which items can be dragged

Implement a `canDrag`
handler that specifies which items can be dragged. This will be called everytime the user
tries to start dragging items, and will pass the items to drag as parameter. Returning false in the handler
will prevent the drag from starting.

```ts jsx
import type { Meta } from "@storybook/react";
import React, { useState } from "react";
import {
  dragAndDropFeature,
  hotkeysCoreFeature,
  keyboardDragAndDropFeature,
  selectionFeature,
  syncDataLoaderFeature,
} from "@headless-tree/core";
import { AssistiveTreeDescription, useTree } from "@headless-tree/react";
import cn from "classnames";

const meta = {
  title: "React/Drag and Drop/Can Drag",
  tags: ["feature/dnd", "basic"],
} satisfies Meta;

export default meta;

// story-start
export const CanDrag = () => {
  const [state, setState] = useState({});
  const tree = useTree<string>({
    state,
    setState,
    rootItemId: "root",
    getItemName: (item) => item.getItemData(),
    isItemFolder: () => true,
    canReorder: true,
    canDrag: (items) =>
      items.every(
        (i) => i.getItemName().endsWith("1") || i.getItemName().endsWith("2"),
      ),
    onDrop: (items, target) => {
      alert(
        `Dropped ${items.map((item) =>
          item.getId(),
        )} on ${target.item.getId()}, ${JSON.stringify(target)}`,
      );
    },
    indent: 20,
    dataLoader: {
      getItem: (itemId) => itemId,
      getChildren: (itemId) => [
        `${itemId}-1`,
        `${itemId}-2`,
        `${itemId}-3`,
        `${itemId}-4`,
        `${itemId}-5`,
        `${itemId}-6`,
      ],
    },
    features: [
      syncDataLoaderFeature,
      selectionFeature,
      hotkeysCoreFeature,
      dragAndDropFeature,
      keyboardDragAndDropFeature,
    ],
  });

  return (
    <>
      <p className="description">
        Only items that end with 1 or 2 can be dragged.
      </p>
      <div {...tree.getContainerProps()} className="tree">
        <AssistiveTreeDescription tree={tree} />
        {tree.getItems().map((item) => (
          <button
            {...item.getProps()}
            key={item.getId()}
            style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }}
          >
            <div
              className={cn("treeitem", {
                focused: item.isFocused(),
                expanded: item.isExpanded(),
                selected: item.isSelected(),
                folder: item.isFolder(),
                drop: item.isDragTarget(),
              })}
            >
              {item.getItemName()}
            </div>
          </button>
        ))}
        <div style={tree.getDragLineStyle()} className="dragline" />
      </div>
    </>
  );
};
```

## Limiting where users can drop items

Similarly, implement a `canDrop`
handler that specifies where items can be dropped. This will be called everytime
the user drags items over a potential drop target (not on every single mousemove event, just when a different drop
target is hovered on), and will pass the items to drop and the target as parameters. Returning false in the handler
will prevent the drop from finalizing and calling the `onDrop` method, as well as visually indicating that the
drop is not allowed at that location.

Note that this doesn't concern [dragging foreign data inside](https://headless-tree.lukasbach.com/llm/dnd/foreign-dnd.md),
which can be controlled with `canDropForeignDragObject`
instead.

```ts jsx
import type { Meta } from "@storybook/react";
import React, { useState } from "react";
import {
  dragAndDropFeature,
  hotkeysCoreFeature,
  keyboardDragAndDropFeature,
  selectionFeature,
  syncDataLoaderFeature,
} from "@headless-tree/core";
import { AssistiveTreeDescription, useTree } from "@headless-tree/react";
import cn from "classnames";

const meta = {
  title: "React/Drag and Drop/Can Drop",
  tags: ["feature/dnd", "basic"],
} satisfies Meta;

export default meta;

// story-start
export const CanDrop = () => {
  const [state, setState] = useState({});
  const tree = useTree<string>({
    state,
    setState,
    rootItemId: "root",
    getItemName: (item) => item.getItemData(),
    isItemFolder: () => true,
    canReorder: true, // TODO! invert for error, still allows to drop even if drop is not shown
    canDrop: (items, { item }) =>
      item.getItemName().endsWith("1") || item.getItemName().endsWith("2"),
    onDrop: (items, target) => {
      alert(
        `Dropped ${items.map((item) =>
          item.getId(),
        )} on ${target.item.getId()}, ${JSON.stringify(target)}`,
      );
    },
    indent: 20,
    dataLoader: {
      getItem: (itemId) => itemId,
      getChildren: (itemId) => [
        `${itemId}-1`,
        `${itemId}-2`,
        `${itemId}-3`,
        `${itemId}-4`,
        `${itemId}-5`,
        `${itemId}-6`,
      ],
    },
    features: [
      syncDataLoaderFeature,
      selectionFeature,
      hotkeysCoreFeature,
      dragAndDropFeature,
      keyboardDragAndDropFeature,
    ],
  });

  return (
    <>
      <p className="description">
        Only on items that end with 1 or 2 can be dropped on.
      </p>
      <div {...tree.getContainerProps()} className="tree">
        <AssistiveTreeDescription tree={tree} />
        {tree.getItems().map((item) => (
          <button
            {...item.getProps()}
            key={item.getId()}
            style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }}
          >
            <div
              className={cn("treeitem", {
                focused: item.isFocused(),
                expanded: item.isExpanded(),
                selected: item.isSelected(),
                folder: item.isFolder(),
                drop: item.isDragTarget(),
              })}
            >
              {item.getItemName()}
            </div>
          </button>
        ))}
        <div style={tree.getDragLineStyle()} className="dragline" />
      </div>
    </>
  );
};
```

## Custom Drag Preview

You can customize the drag preview by providing a `setDragImage` to your tree options. It will be called
when the user starts dragging, with the dragged items as parameter. The functions return parameters will
be used to invoke [`dataTransfer.setDragImage`](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/setDragImage)
to set the drag preview.

```ts
setDragImage: (draggedItems) => ({
  imgElement: document.getElementById("dragpreview"),
})
```

The provided element does not have to be an image element, but if it is not, it will need to be visible in the viewport.
In this case, you will want to move it with CSS off-screen to hide it whenever the user is not dragging anything.

```ts jsx
import type { Meta } from "@storybook/react";
import React, { useState } from "react";
import {
  TreeState,
  dragAndDropFeature,
  hotkeysCoreFeature,
  keyboardDragAndDropFeature,
  selectionFeature,
  syncDataLoaderFeature,
} from "@headless-tree/core";
import { AssistiveTreeDescription, useTree } from "@headless-tree/react";
import cn from "classnames";

const meta = {
  title: "React/Drag and Drop/Drag Preview",
  tags: ["feature/dnd"],
} satisfies Meta;

export default meta;

// story-start
export const DragPreview = () => {
  const [state, setState] = useState<Partial<TreeState<any>>>({
    expandedItems: ["root-1", "root-1-2"],
    selectedItems: ["root-1-2-1", "root-1-2-2", "root-1-2-3", "root-1-2-4"],
  });
  const tree = useTree<string>({
    state,
    setState,
    rootItemId: "root",
    getItemName: (item) => item.getItemData(),
    isItemFolder: () => true,
    canReorder: true,
    onDrop: (items, target) => {
      alert(
        `Dropped ${items.map((item) =>
          item.getId(),
        )} on ${target.item.getId()}, ${JSON.stringify(target)}`,
      );
    },
    setDragImage: () => ({
      imgElement: document.getElementById("dragpreview")!,
      xOffset: -20,
    }),
    indent: 20,
    dataLoader: {
      getItem: (itemId) => itemId,
      getChildren: (itemId) => [
        `${itemId}-1`,
        `${itemId}-2`,
        `${itemId}-3`,
        `${itemId}-4`,
      ],
    },
    features: [
      syncDataLoaderFeature,
      selectionFeature,
      hotkeysCoreFeature,
      dragAndDropFeature,
      keyboardDragAndDropFeature,
    ],
  });

  const draggedItems = tree.getState().dnd?.draggedItems;

  return (
    <>
      <p className="description">
        This sample utilizes custom drag previews that are defined with the
        option setDragImage. Start dragging items to see the custom preview.
      </p>
      <div {...tree.getContainerProps()} className="tree">
        <AssistiveTreeDescription tree={tree} />
        {tree.getItems().map((item) => (
          <button
            {...item.getProps()}
            key={item.getId()}
            style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }}
          >
            <div
              className={cn("treeitem", {
                focused: item.isFocused(),
                expanded: item.isExpanded(),
                selected: item.isSelected(),
                folder: item.isFolder(),
                drop: item.isDragTarget(),
              })}
            >
              {item.getItemName()}
            </div>
          </button>
        ))}
        <div style={tree.getDragLineStyle()} className="dragline" />
        <div
          id="dragpreview"
          style={{
            // move the drag preview off-screen by default
            position: "absolute",
            left: "-9999px",

            // styling to make it look nice
            width: "200px",
            background: "white",
            borderRadius: "4px",
            border: "1px solid #eee",
            padding: "4px",
          }}
        >
          Dragging{" "}
          {draggedItems
            ?.slice(0, 3)
            .map((item) => item.getItemName())
            .join(", ")}
          {(draggedItems?.length ?? 0) > 3 &&
            ` and ${(draggedItems?.length ?? 0) - 3} more`}
        </div>
      </div>
    </>
  );
};
```

## Separate Drag Handle

By default, drag-and-drop is initiated when the user clicks and drags anywhere on a tree item. If you want more precise
control over where users can initiate dragging, you can set `seperateDragHandle`
to `true` in your tree configuration.

When enabled, the drag event handlers are removed from `item.getProps()` and you must manually apply them to a specific
drag handle element using `item.getDragHandleProps()`. This allows you to create dedicated drag handles (like icons or grip handles)
while keeping the rest of the item interactive for other purposes like selection or expansion.

```tsx
const tree = useTree({
  // ... other config
  seperateDragHandle: true,
});

// In your render:
<div {...item.getProps()}>
  <span {...item.getDragHandleProps()} className="drag-handle">
    ⋮⋮
  </span>
  <span>{item.getItemName()}</span>
</div>
```

This is particularly useful when you want to:
- Provide a clear visual indicator of where to drag
- Keep clickable content (like buttons or links) in the item that shouldn't trigger dragging
- Create more precise drag interactions in complex item layouts

```ts jsx
import type { Meta } from "@storybook/react";
import React, { useState } from "react";
import {
  TreeState,
  dragAndDropFeature,
  hotkeysCoreFeature,
  keyboardDragAndDropFeature,
  selectionFeature,
  syncDataLoaderFeature,
} from "@headless-tree/core";
import { AssistiveTreeDescription, useTree } from "@headless-tree/react";
import cn from "classnames";

const meta = {
  title: "React/Drag and Drop/Seperate Drag Handle",
  tags: ["feature/dnd", "basic"],
} satisfies Meta;

export default meta;

// story-start
export const SeperateDragHandle = () => {
  const [state, setState] = useState<Partial<TreeState<any>>>({
    expandedItems: ["root-1", "root-1-2"],
    selectedItems: ["root-1-2-1", "root-1-2-2"],
  });
  const tree = useTree<string>({
    state,
    setState,
    rootItemId: "root",
    getItemName: (item) => item.getItemData(),
    isItemFolder: () => true,
    canReorder: true,
    seperateDragHandle: true,
    onDrop: (items, target) => {
      alert(
        `Dropped ${items.map((item) =>
          item.getId(),
        )} on ${target.item.getId()}, ${JSON.stringify(target)}`,
      );
    },
    indent: 20,
    dataLoader: {
      getItem: (itemId) => itemId,
      getChildren: (itemId) => [
        `${itemId}-1`,
        `${itemId}-2`,
        `${itemId}-3`,
        `${itemId}-4`,
      ],
    },
    features: [
      syncDataLoaderFeature,
      selectionFeature,
      hotkeysCoreFeature,
      dragAndDropFeature,
      keyboardDragAndDropFeature,
    ],
  });

  return (
    <div {...tree.getContainerProps()} className="tree">
      <AssistiveTreeDescription tree={tree} />
      {tree.getItems().map((item) => (
        <button
          {...item.getProps()}
          key={item.getId()}
          style={{
            paddingLeft: `${item.getItemMeta().level * 20}px`,
            display: "flex",
            alignItems: "center",
            gap: "8px",
          }}
        >
          <span
            {...item.getDragHandleProps()}
            className="drag-handle"
            title="Drag handle - click and drag to move this item"
          >
            ⋮⋮
          </span>
          <div
            className={cn("treeitem", {
              focused: item.isFocused(),
              expanded: item.isExpanded(),
              selected: item.isSelected(),
              folder: item.isFolder(),
              drop: item.isDragTarget(),
            })}
            style={{ flex: 1 }}
          >
            {item.getItemName()}
          </div>
        </button>
      ))}
      <div style={tree.getDragLineStyle()} className="dragline" />
    </div>
  );
};
```