diff --git a/docs/src/demos/Experiments/GlobalDragHandle/DragHandle.js b/docs/src/demos/Experiments/GlobalDragHandle/DragHandle.js
new file mode 100644
index 00000000..7d146113
--- /dev/null
+++ b/docs/src/demos/Experiments/GlobalDragHandle/DragHandle.js
@@ -0,0 +1,158 @@
+import { Extension } from '@tiptap/core'
+import { NodeSelection, Plugin } from 'prosemirror-state'
+import { serializeForClipboard } from 'prosemirror-view/src/clipboard'
+
+function removeNode(node) {
+ node.parentNode.removeChild(node)
+}
+
+function absoluteRect(node) {
+ const data = node.getBoundingClientRect()
+
+ return {
+ top: data.top,
+ left: data.left,
+ width: data.width,
+ }
+}
+
+export default Extension.create({
+ addProseMirrorPlugins() {
+ function blockPosAtCoords(coords, view) {
+ const pos = view.posAtCoords(coords)
+ let node = view.domAtPos(pos.pos)
+
+ node = node.node
+
+ while (node && node.parentNode) {
+ if (node.parentNode?.classList?.contains('ProseMirror')) { // todo
+ break
+ }
+
+ node = node.parentNode
+ }
+
+ if (node && node.nodeType === 1) {
+ const desc = view.docView.nearestDesc(node, true)
+
+ if (!(!desc || desc === view.docView)) {
+ return desc.posBefore
+ }
+ }
+ return null
+ }
+
+ function dragStart(e, view) {
+ view.composing = true
+
+ if (!e.dataTransfer) {
+ return
+ }
+
+ const coords = { left: e.clientX + 50, top: e.clientY }
+ const pos = blockPosAtCoords(coords, view)
+
+ if (pos != null) {
+ view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, pos)))
+
+ const slice = view.state.selection.content()
+ const { dom, text } = serializeForClipboard(view, slice)
+
+ e.dataTransfer.clearData()
+ e.dataTransfer.setData('text/html', dom.innerHTML)
+ e.dataTransfer.setData('text/plain', text)
+
+ const el = document.querySelector('.ProseMirror-selectednode')
+ e.dataTransfer?.setDragImage(el, 0, 0)
+
+ view.dragging = { slice, move: true }
+ }
+ }
+
+ let dropElement
+ const WIDTH = 28
+
+ return [
+ new Plugin({
+ view(editorView) {
+ const element = document.createElement('div')
+
+ element.draggable = 'true'
+ element.classList.add('global-drag-handle')
+ element.addEventListener('dragstart', e => dragStart(e, editorView))
+ dropElement = element
+ document.body.appendChild(dropElement)
+
+ return {
+ // update(view, prevState) {
+ // },
+ destroy() {
+ removeNode(dropElement)
+ dropElement = null
+ },
+ }
+ },
+ props: {
+ handleDrop(view, event, slice, moved) {
+ if (moved) {
+ // setTimeout(() => {
+ // console.log('remove selection')
+ // view.dispatch(view.state.tr.deleteSelection())
+ // }, 50)
+ }
+ },
+ // handlePaste() {
+ // alert(2)
+ // },
+ handleDOMEvents: {
+ // drop(view, event) {
+ // setTimeout(() => {
+ // const node = document.querySelector('.ProseMirror-hideselection')
+ // if (node) {
+ // node.classList.remove('ProseMirror-hideselection')
+ // }
+ // }, 50)
+ // },
+ mousemove(view, event) {
+ const coords = {
+ left: event.clientX + WIDTH + 50,
+ top: event.clientY,
+ }
+ const pos = view.posAtCoords(coords)
+
+ if (pos) {
+ let node = view.domAtPos(pos?.pos)
+
+ if (node) {
+ node = node.node
+ while (node && node.parentNode) {
+ if (node.parentNode?.classList?.contains('ProseMirror')) { // todo
+ break
+ }
+ node = node.parentNode
+ }
+
+ if (node instanceof Element) {
+ const cstyle = window.getComputedStyle(node)
+ const lineHeight = parseInt(cstyle.lineHeight, 10)
+ // const top = parseInt(cstyle.marginTop, 10) + parseInt(cstyle.paddingTop, 10)
+ const top = 0
+ const rect = absoluteRect(node)
+ const win = node.ownerDocument.defaultView
+
+ rect.top += win.pageYOffset + ((lineHeight - 24) / 2) + top
+ rect.left += win.pageXOffset
+ rect.width = `${WIDTH}px`
+
+ dropElement.style.left = `${-WIDTH + rect.left}px`
+ dropElement.style.top = `${rect.top}px`
+ }
+ }
+ }
+ },
+ },
+ },
+ }),
+ ]
+ },
+})
diff --git a/docs/src/demos/Experiments/GlobalDragHandle/index.vue b/docs/src/demos/Experiments/GlobalDragHandle/index.vue
new file mode 100644
index 00000000..90fec8b2
--- /dev/null
+++ b/docs/src/demos/Experiments/GlobalDragHandle/index.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/src/docPages/experiments.md b/docs/src/docPages/experiments.md
index 871b6f5f..0e9a9acd 100644
--- a/docs/src/docPages/experiments.md
+++ b/docs/src/docPages/experiments.md
@@ -4,6 +4,7 @@ Congratulations! You’ve found our playground with a list of experiments. Be aw
## New
* [Linter](/experiments/linter)
* [Multiple editors](/experiments/multiple-editors)
+* [Global drag handle](/experiments/global-drag-handle)
* [@tiptap/extension-slash-command?](/experiments/commands)
* [@tiptap/extension-iframe?](/experiments/embeds)
* [@tiptap/extension-toggle-list?](/experiments/details)
diff --git a/docs/src/docPages/experiments/global-drag-handle.md b/docs/src/docPages/experiments/global-drag-handle.md
new file mode 100644
index 00000000..505ee962
--- /dev/null
+++ b/docs/src/docPages/experiments/global-drag-handle.md
@@ -0,0 +1,5 @@
+# GlobalDragHandle
+
+⚠️ Experiment
+
+