Skip to main content

DragDrop

Device-agnostic drag-and-drop for Roblox UI. A singleton that moves an opaque Payload from a registered source to a registered target across mouse, touch, gamepad, and keyboard, emitting intent through the target's OnDrop. It never inspects a payload past its Tags, so one registration handles any kind of content.

Register a source and/or a target on a GuiObject and keep the returned Unregister alive for as long as the element should participate. In a component framework, insert it into your cleanup scope:

local DragDrop = require(Packages.DragDrop)

local disconnectSource = DragDrop.RegisterSource(slot, {
	GetPayload = function()
		return { Tags = { "Item" }, Guid = guid, FromIndex = i }
	end,
	BuildGhost = function(payload, source) return MyItemGhost(payload) end,
	OnActivated = openContextMenu, -- click / A-press without a lift
})

local disconnectTarget = DragDrop.RegisterTarget(slot, {
	Accepts = { "Item" }, -- only takes item drags
	OnDrop = function(payload, target) MoveItem(payload, target) end,
	Metadata = { Index = i },
})

Where to read more

Types

Payload

type Payload = {
Tags{string},
[string]any
}

The opaque value carried by a drag. Tags is the only field the system reads: it lists the payload's kinds for target compatibility — a target's Accepts takes the drag if it shares at least one tag. Everything else is yours to shape (guids, indices, a display name for your ghost, ...).

Because Tags travels with the payload and GetPayload runs on every lift, a source whose contents vary can return different tags per lift — e.g. a bag slot returns { "Item", "Food" } for a ration and { "Item", "Weapon" } for a sword, and each matches the right target.

TargetDescriptor

interface TargetDescriptor {
InstanceGuiObject--

The registered target the drop resolved onto.

Metadata{[any]any}--

Whatever Metadata the target registered with.

}

Handed to a target's OnDrop and returned by DragDrop.GetHoveredTarget.

InputMode

type InputMode = "Automatic" | "Scriptable"

Automatic (default) wires the pointer and selection backends to real device input. Scriptable disables them so the only way a drag starts or moves is through DragDrop.BeginDrag and the returned DragHandle — like putting a camera in Scriptable mode.

Config

interface Config {
DragThresholdPxnumber?--

Mouse: movement past this (in pixels) lifts. Default 8.

TouchSlopPxnumber?--

Touch: movement past this before the hold fires = scroll, not lift. Default 12.

LongPressSecondsnumber?--

Touch: hold duration that lifts. Default 0.4.

SnapSpeednumber?--

Selection mode: slot-to-slot ghost spring speed. Default 30.

GhostContainerLayerCollector?--

Parent for ghost mounts. nil = an internal high-DisplayOrder ScreenGui.

GamepadLiftKeyCodeEnum.KeyCode?--

Button that lifts the selected source. Default ButtonX.

GamepadDropKeyCodeEnum.KeyCode?--

Button that drops on the selected target. Default ButtonA.

GamepadCancelKeyCodeEnum.KeyCode?--

Button that cancels a drag. Default ButtonB.

KeyboardLiftKeyCodeEnum.KeyCode?--

Key that toggles lift/drop in grab mode. Default Return.

KeyboardCancelKeyCodeEnum.KeyCode?--

Optional explicit keyboard cancel key. Default nil.

}

Behavior knobs only — nothing here styles the ghost (that lives in BuildGhost).

DragHandle

interface DragHandle {
MoveTo(
selfDragHandle,
positionVector2
) → ()--

Move the ghost to a screen point and hit-test for hover.

SetHoveredTarget(
selfDragHandle,
targetGuiObject?
) → boolean--

Set hover explicitly; false if the target is invalid/incompatible.

Drop(selfDragHandle) → boolean--

End on the current hover; false if there was no valid target (reverted).

Cancel(selfDragHandle) → ()--

Revert this drag.

IsActive(selfDragHandle) → boolean--

Whether this handle still controls the live drag.

GetPayload(selfDragHandle) → Payload--

The payload this drag carries.

}

A programmatic controller for a single drag, returned by DragDrop.BeginDrag. Every method becomes a no-op (and the mutating ones return false) once the drag has ended.

Unregister

type Unregister = () → ()

Returned by DragDrop.RegisterSource and DragDrop.RegisterTarget. Call it to tear the registration down.

Properties

ActivePayloadChanged

DragDrop.ActivePayloadChanged: Signal<(payloadPayload?)>

Fires whenever the airborne payload changes — the new payload on lift, nil on drop or cancel.

It does not fire on connect, so read the current value with DragDrop.GetActivePayload when you subscribe. It is a plain [Signal], so it works with anything that consumes signals — :Once, :Wait, a Janitor, a reactive binding.

-- Dim every incompatible slot while a drag is live. Seed from the getter, then
-- keep it in sync from the signal.
local function repaint(payload: DragDrop.Payload?)
	for _, slot in slots do
		slot.Dimmed = payload ~= nil and not accepts(slot, payload)
	end
end
repaint(DragDrop.GetActivePayload())
local conn = DragDrop.ActivePayloadChanged:Connect(repaint)

-- later:
conn:Disconnect()

HoveredTargetChanged

DragDrop.HoveredTargetChanged: Signal<(targetTargetDescriptor?)>

Fires whenever the hovered target changes — the new TargetDescriptor on enter, nil on leave.

It does not fire on connect, so read the current value with DragDrop.GetHoveredTarget when you subscribe.

-- Move a "drop here" highlight to whatever the ghost is over.
local conn = DragDrop.HoveredTargetChanged:Connect(function(target)
	highlight.Visible = target ~= nil
	if target then highlight.Parent = target.Instance end
end)

DragStarted

DragDrop.DragStarted: Signal<(
payloadPayload,
sourceGuiObject
)>

Fires when a payload goes airborne.

DragDrop.DragStarted:Connect(function(payload, source)
	playPickupSound()
	source.BackgroundTransparency = 0.6 -- fade the origin slot while lifted
end)

Dropped

DragDrop.Dropped: Signal<(
payloadPayload,
)>

Fires when a payload lands on a valid target, immediately after that target's OnDrop has run.

-- A global listener for analytics, independent of any one target's OnDrop.
DragDrop.Dropped:Connect(function(payload, target)
	Analytics:Log("item_moved", {
		guid = payload.Guid,
		to = target.Metadata.Index,
	})
end)

DragEnded

DragDrop.DragEnded: Signal<(
payloadPayload,
)>

Fires on every drag end. A nil target means the drag was cancelled or reverted.

This is the last event of a drag. End-of-drag order:

  1. Reactive state clears — GetActivePayload/GetHoveredTarget go nil and DragDrop.ActivePayloadChanged/DragDrop.HoveredTargetChanged fire (ghost/scroll-lock teardown has already run).
  2. The target's OnDrop runs, then DragDrop.Dropped fires.
  3. The source's OnDragEnd runs, then DragEnded fires.

Because those change signals fire before OnDrop mutates your data, a view that repaints only from them will still show pre-drop data — repaint on DragEnded (or from your own data layer) to pick up the result of the drop.

DragDrop.DragEnded:Connect(function(payload, target)
	if target == nil then
		playRevertSound() -- cancelled or reverted
	end
	repaintInventory() -- OnDrop has already applied the move by now
end)

Functions

RegisterSource

DragDrop.RegisterSource(
instanceGuiObject,
optionsSourceOptions
) → () → ()

Types

interface SourceOptions {
GetPayload() → Payload?--

Returns the payload to lift, or nil to refuse the lift (empty slot, disabled, ...). Called on press.

OnActivated(() → ())?--

Press + release without crossing the drag threshold (a "click").

OnLift((payloadPayload) → ())?--

Fired the moment the payload goes airborne.

OnDragEnd((
payloadPayload,
droppedboolean
) → ())?--

Fired on every drag end. dropped=false means cancelled/reverted.

BuildGhost((
payloadPayload,
sourceGuiObject
) → GuiObject)?--

Builds the dragged proxy. Strongly recommended; nil falls back to a bare debug plate.

}

Marks instance (and its descendants) as something that can be picked up. Returns a function that unregisters it; the source also auto-unregisters if the instance is destroyed.

local unregister = DragDrop.RegisterSource(slot, {
	GetPayload = function()
		if slot.IsEmpty then return nil end -- refuse the lift
		return { Tags = { "Item" }, Guid = slot.Guid, FromIndex = i }
	end,
	BuildGhost = function(payload) return MyItemGhost(payload) end,
	OnActivated = function() openContextMenu(i) end, -- click without a lift
})

-- later, when the slot goes away:
unregister()

RegisterTarget

DragDrop.RegisterTarget(
instanceGuiObject,
optionsTargetOptions
) → () → ()

Types

interface TargetOptions {
Accepts{string}?--

Tags this target accepts; matches if it shares at least one tag with the payload's Tags. Empty or omitted accepts any payload.

CanDrop((payloadPayload) → boolean)?--

Optional finer gate, checked in addition to Accepts.

OnDrop(
payloadPayload,
) → ()--

Called when a compatible payload is dropped here.

OnHoverChanged((payloadPayload?) → ())?--

payload on enter, nil on leave.

Metadata{[any]any}?--

Arbitrary data surfaced back through TargetDescriptor.Metadata.

}

Marks instance (and its descendants) as somewhere a payload can be dropped. Returns a function that unregisters it; the target also auto-unregisters if the instance is destroyed.

local unregister = DragDrop.RegisterTarget(slot, {
	Accepts = { "Item" }, -- only item drags; omit to accept any payload
	CanDrop = function(payload) return not slot.Locked end, -- finer gate
	OnDrop = function(payload, target)
		MoveItem(payload.Guid, target.Metadata.Index)
	end,
	OnHoverChanged = function(payload)
		slot.Highlight.Visible = payload ~= nil -- nil on leave
	end,
	Metadata = { Index = i },
})

BeginDrag

DragDrop.BeginDrag(
instanceGuiObject,--

a registered source (or a descendant of one)

positionVector2?--

optional initial ghost position (canonical space)

) → DragHandle?

Lifts a registered source programmatically and returns a DragHandle, or nil if the lift is refused (not registered, GetPayload returned nil, or a drag is already active).

  • With position: a pointer-controlled drag whose ghost follows the point you feed via handle:MoveTo. The automatic backends leave it alone.
  • Without position: a selection grab-mode drag. In Automatic mode the gamepad/keyboard backend drives it (selection nav moves the ghost); in Scriptable mode you drive it with handle:SetHoveredTarget and handle:Drop.
-- Pointer-controlled: feed the ghost a position yourself.
local handle = DragDrop.BeginDrag(slot, UserInputService:GetMouseLocation())
if handle then
	handle:MoveTo(UserInputService:GetMouseLocation()) -- each frame
	handle:Drop() -- lands on whatever it's hovering
end

-- Selection grab-mode (no position): drive it explicitly under Scriptable mode.
local handle = DragDrop.BeginDrag(sourceSlot)
if handle then
	handle:SetHoveredTarget(destSlot)
	handle:Drop()
end

SetInputMode

DragDrop.SetInputMode(modeInputMode) → ()

Switches between "Automatic" (device backends wired to real input) and "Scriptable" (backends off; drags driven only through DragDrop.BeginDrag). Any active drag is cancelled across the switch.

-- In a test: take control away from real input, then drive a drag by hand.
DragDrop.SetInputMode("Scriptable")
local handle = DragDrop.BeginDrag(sourceSlot)
handle:SetHoveredTarget(destSlot)
handle:Drop()
DragDrop.SetInputMode("Automatic") -- hand input back

GetInputMode

DragDrop.GetInputMode() → InputMode
if DragDrop.GetInputMode() == "Automatic" then
	-- real device input is live
end

Drop

DragDrop.Drop() → boolean

Ends the active drag on the currently hovered target. Returns true if it landed on a valid target, false if there was none (a revert). No-op when idle.

-- Bind a "confirm" key to commit whatever drag is in flight.
local landed = DragDrop.Drop()
if not landed then
	-- reverted: nothing valid under the ghost
end

Cancel

DragDrop.Cancel() → ()

Reverts the active drag, or aborts a pending press. No-op when idle.

-- Escape backs out of whatever is in flight.
UserInputService.InputBegan:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.Escape then
		DragDrop.Cancel()
	end
end)

IsDragging

DragDrop.IsDragging() → boolean
-- Suppress a hover tooltip while something is airborne.
if not DragDrop.IsDragging() then
	showTooltip(slot)
end

GetActivePayload

DragDrop.GetActivePayload() → Payload?

The payload currently airborne, or nil when idle.

local payload = DragDrop.GetActivePayload()
if payload and table.find(payload.Tags, "Item") then
	-- an item is in flight right now
end

GetHoveredTarget

DragDrop.GetHoveredTarget() → TargetDescriptor?

The valid target the drag is currently over, or nil.

local target = DragDrop.GetHoveredTarget()
if target then
	print("would drop into slot", target.Metadata.Index)
end

ConsumeActivation

DragDrop.ConsumeActivation() → boolean

True if a drag ended within the last few hundredths of a second — use it to swallow a stray Button.Activated that fires right after a drop, so a drag doesn't also read as a click.

button.Activated:Connect(function()
	if DragDrop.ConsumeActivation() then
		return -- this "click" was really the tail of a drag; ignore it
	end
	openSlot()
end)

Configure

DragDrop.Configure(configConfig) → ()

Applies a partial Config over the current settings. Errors on unknown keys or wrong value types. Behavior knobs only — ghost appearance lives in SourceOptions.BuildGhost.

DragDrop.Configure({
	DragThresholdPx = 12, -- a little more slop before a mouse lift
	LongPressSeconds = 0.3, -- quicker touch lift
	GhostContainer = playerGui.DragLayer, -- mount ghosts here
})

ResetConfig

DragDrop.ResetConfig() → ()

Restores every Config key to its default. This is also the only way to clear an optional key such as GhostContainerConfigure cannot accept nil for them, since nil values vanish from Luau tables.

DragDrop.Configure({ GhostContainer = myLayer })
-- ...
DragDrop.ResetConfig() -- back to defaults, GhostContainer cleared to nil
Show raw api
{
    "functions": [
        {
            "name": "RegisterSource",
            "desc": "Marks `instance` (and its descendants) as something that can be picked up.\nReturns a function that unregisters it; the source also auto-unregisters if the\ninstance is destroyed.\n\n```lua\nlocal unregister = DragDrop.RegisterSource(slot, {\n\tGetPayload = function()\n\t\tif slot.IsEmpty then return nil end -- refuse the lift\n\t\treturn { Tags = { \"Item\" }, Guid = slot.Guid, FromIndex = i }\n\tend,\n\tBuildGhost = function(payload) return MyItemGhost(payload) end,\n\tOnActivated = function() openContextMenu(i) end, -- click without a lift\n})\n\n-- later, when the slot goes away:\nunregister()\n```",
            "params": [
                {
                    "name": "instance",
                    "desc": "",
                    "lua_type": "GuiObject"
                },
                {
                    "name": "options",
                    "desc": "",
                    "lua_type": "SourceOptions"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "() -> ()"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 86,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        },
        {
            "name": "RegisterTarget",
            "desc": "Marks `instance` (and its descendants) as somewhere a payload can be dropped.\nReturns a function that unregisters it; the target also auto-unregisters if the\ninstance is destroyed.\n\n```lua\nlocal unregister = DragDrop.RegisterTarget(slot, {\n\tAccepts = { \"Item\" }, -- only item drags; omit to accept any payload\n\tCanDrop = function(payload) return not slot.Locked end, -- finer gate\n\tOnDrop = function(payload, target)\n\t\tMoveItem(payload.Guid, target.Metadata.Index)\n\tend,\n\tOnHoverChanged = function(payload)\n\t\tslot.Highlight.Visible = payload ~= nil -- nil on leave\n\tend,\n\tMetadata = { Index = i },\n})\n```",
            "params": [
                {
                    "name": "instance",
                    "desc": "",
                    "lua_type": "GuiObject"
                },
                {
                    "name": "options",
                    "desc": "",
                    "lua_type": "TargetOptions"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "() -> ()"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 113,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        },
        {
            "name": "BeginDrag",
            "desc": "Lifts a registered source programmatically and returns a [DragHandle], or nil\nif the lift is refused (not registered, `GetPayload` returned nil, or a drag is\nalready active).\n\n- With `position`: a **pointer-controlled** drag whose ghost follows the point\n  you feed via `handle:MoveTo`. The automatic backends leave it alone.\n- Without `position`: a **selection grab-mode** drag. In Automatic mode the\n  gamepad/keyboard backend drives it (selection nav moves the ghost); in\n  Scriptable mode you drive it with `handle:SetHoveredTarget` and `handle:Drop`.\n\n```lua\n-- Pointer-controlled: feed the ghost a position yourself.\nlocal handle = DragDrop.BeginDrag(slot, UserInputService:GetMouseLocation())\nif handle then\n\thandle:MoveTo(UserInputService:GetMouseLocation()) -- each frame\n\thandle:Drop() -- lands on whatever it's hovering\nend\n\n-- Selection grab-mode (no position): drive it explicitly under Scriptable mode.\nlocal handle = DragDrop.BeginDrag(sourceSlot)\nif handle then\n\thandle:SetHoveredTarget(destSlot)\n\thandle:Drop()\nend\n```",
            "params": [
                {
                    "name": "instance",
                    "desc": "a registered source (or a descendant of one)",
                    "lua_type": "GuiObject"
                },
                {
                    "name": "position",
                    "desc": "optional initial ghost position (canonical space)",
                    "lua_type": "Vector2?"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "DragHandle?"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 150,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        },
        {
            "name": "SetInputMode",
            "desc": "Switches between `\"Automatic\"` (device backends wired to real input) and\n`\"Scriptable\"` (backends off; drags driven only through [DragDrop.BeginDrag]).\nAny active drag is cancelled across the switch.\n\n```lua\n-- In a test: take control away from real input, then drive a drag by hand.\nDragDrop.SetInputMode(\"Scriptable\")\nlocal handle = DragDrop.BeginDrag(sourceSlot)\nhandle:SetHoveredTarget(destSlot)\nhandle:Drop()\nDragDrop.SetInputMode(\"Automatic\") -- hand input back\n```",
            "params": [
                {
                    "name": "mode",
                    "desc": "",
                    "lua_type": "InputMode"
                }
            ],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 170,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        },
        {
            "name": "GetInputMode",
            "desc": "```lua\nif DragDrop.GetInputMode() == \"Automatic\" then\n\t-- real device input is live\nend\n```",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "InputMode"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 183,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        },
        {
            "name": "Drop",
            "desc": "Ends the active drag on the currently hovered target. Returns `true` if it\nlanded on a valid target, `false` if there was none (a revert). No-op when idle.\n\n```lua\n-- Bind a \"confirm\" key to commit whatever drag is in flight.\nlocal landed = DragDrop.Drop()\nif not landed then\n\t-- reverted: nothing valid under the ghost\nend\n```",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "boolean"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 201,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        },
        {
            "name": "Cancel",
            "desc": "Reverts the active drag, or aborts a pending press. No-op when idle.\n\n```lua\n-- Escape backs out of whatever is in flight.\nUserInputService.InputBegan:Connect(function(input)\n\tif input.KeyCode == Enum.KeyCode.Escape then\n\t\tDragDrop.Cancel()\n\tend\nend)\n```",
            "params": [],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 218,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        },
        {
            "name": "IsDragging",
            "desc": "```lua\n-- Suppress a hover tooltip while something is airborne.\nif not DragDrop.IsDragging() then\n\tshowTooltip(slot)\nend\n```",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "boolean"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 234,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        },
        {
            "name": "GetActivePayload",
            "desc": "The payload currently airborne, or nil when idle.\n\n```lua\nlocal payload = DragDrop.GetActivePayload()\nif payload and table.find(payload.Tags, \"Item\") then\n\t-- an item is in flight right now\nend\n```",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Payload?"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 250,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        },
        {
            "name": "GetHoveredTarget",
            "desc": "The valid target the drag is currently over, or nil.\n\n```lua\nlocal target = DragDrop.GetHoveredTarget()\nif target then\n\tprint(\"would drop into slot\", target.Metadata.Index)\nend\n```",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "TargetDescriptor?"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 266,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        },
        {
            "name": "ConsumeActivation",
            "desc": "True if a drag ended within the last few hundredths of a second — use it to\nswallow a stray `Button.Activated` that fires right after a drop, so a drag\ndoesn't also read as a click.\n\n```lua\nbutton.Activated:Connect(function()\n\tif DragDrop.ConsumeActivation() then\n\t\treturn -- this \"click\" was really the tail of a drag; ignore it\n\tend\n\topenSlot()\nend)\n```",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "boolean"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 333,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        },
        {
            "name": "Configure",
            "desc": "Applies a partial [Config] over the current settings. Errors on unknown keys\nor wrong value types. Behavior knobs only — ghost appearance lives in\n`SourceOptions.BuildGhost`.\n\n```lua\nDragDrop.Configure({\n\tDragThresholdPx = 12, -- a little more slop before a mouse lift\n\tLongPressSeconds = 0.3, -- quicker touch lift\n\tGhostContainer = playerGui.DragLayer, -- mount ghosts here\n})\n```",
            "params": [
                {
                    "name": "config",
                    "desc": "",
                    "lua_type": "Config"
                }
            ],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 352,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        },
        {
            "name": "ResetConfig",
            "desc": "Restores every [Config] key to its default. This is also the only way to\n*clear* an optional key such as `GhostContainer` — [Configure](/api/DragDrop#Configure)\ncannot accept nil for them, since nil values vanish from Luau tables.\n\n```lua\nDragDrop.Configure({ GhostContainer = myLayer })\n-- ...\nDragDrop.ResetConfig() -- back to defaults, GhostContainer cleared to nil\n```",
            "params": [],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 368,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        }
    ],
    "properties": [
        {
            "name": "ActivePayloadChanged",
            "desc": "Fires whenever the airborne payload changes — the new payload on lift, nil on\ndrop or cancel.\n\nIt does **not** fire on connect, so read the current value with\n[DragDrop.GetActivePayload] when you subscribe. It is a plain [Signal], so it\nworks with anything that consumes signals — `:Once`, `:Wait`, a Janitor, a\nreactive binding.\n\n```lua\n-- Dim every incompatible slot while a drag is live. Seed from the getter, then\n-- keep it in sync from the signal.\nlocal function repaint(payload: DragDrop.Payload?)\n\tfor _, slot in slots do\n\t\tslot.Dimmed = payload ~= nil and not accepts(slot, payload)\n\tend\nend\nrepaint(DragDrop.GetActivePayload())\nlocal conn = DragDrop.ActivePayloadChanged:Connect(repaint)\n\n-- later:\nconn:Disconnect()\n```",
            "lua_type": "Signal<(payload: Payload?)>",
            "source": {
                "line": 294,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        },
        {
            "name": "HoveredTargetChanged",
            "desc": "Fires whenever the hovered target changes — the new [TargetDescriptor] on enter,\nnil on leave.\n\nIt does **not** fire on connect, so read the current value with\n[DragDrop.GetHoveredTarget] when you subscribe.\n\n```lua\n-- Move a \"drop here\" highlight to whatever the ghost is over.\nlocal conn = DragDrop.HoveredTargetChanged:Connect(function(target)\n\thighlight.Visible = target ~= nil\n\tif target then highlight.Parent = target.Instance end\nend)\n```",
            "lua_type": "Signal<(target: TargetDescriptor?)>",
            "source": {
                "line": 313,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        },
        {
            "name": "DragStarted",
            "desc": "Fires when a payload goes airborne.\n\n```lua\nDragDrop.DragStarted:Connect(function(payload, source)\n\tplayPickupSound()\n\tsource.BackgroundTransparency = 0.6 -- fade the origin slot while lifted\nend)\n```",
            "lua_type": "Signal<(payload: Payload, source: GuiObject)>",
            "source": {
                "line": 384,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        },
        {
            "name": "Dropped",
            "desc": "Fires when a payload lands on a valid target, immediately after that target's\n`OnDrop` has run.\n\n```lua\n-- A global listener for analytics, independent of any one target's OnDrop.\nDragDrop.Dropped:Connect(function(payload, target)\n\tAnalytics:Log(\"item_moved\", {\n\t\tguid = payload.Guid,\n\t\tto = target.Metadata.Index,\n\t})\nend)\n```",
            "lua_type": "Signal<(payload: Payload, target: TargetDescriptor)>",
            "source": {
                "line": 402,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        },
        {
            "name": "DragEnded",
            "desc": "Fires on every drag end. A nil target means the drag was cancelled or reverted.\n\nThis is the **last** event of a drag. End-of-drag order:\n1. Reactive state clears — `GetActivePayload`/`GetHoveredTarget` go nil and\n   [DragDrop.ActivePayloadChanged]/[DragDrop.HoveredTargetChanged] fire\n   (ghost/scroll-lock teardown has already run).\n2. The target's `OnDrop` runs, then [DragDrop.Dropped] fires.\n3. The source's `OnDragEnd` runs, then `DragEnded` fires.\n\nBecause those change signals fire *before* `OnDrop` mutates your data, a view\nthat repaints only from them will still show pre-drop data — repaint on\n`DragEnded` (or from your own data layer) to pick up the result of the drop.\n\n```lua\nDragDrop.DragEnded:Connect(function(payload, target)\n\tif target == nil then\n\t\tplayRevertSound() -- cancelled or reverted\n\tend\n\trepaintInventory() -- OnDrop has already applied the move by now\nend)\n```",
            "lua_type": "Signal<(payload: Payload, target: TargetDescriptor?)>",
            "source": {
                "line": 429,
                "path": "lib/dragdrop/src/DragDrop.luau"
            }
        }
    ],
    "types": [
        {
            "name": "Payload",
            "desc": "The opaque value carried by a drag. `Tags` is the only field the system reads:\nit lists the payload's kinds for target compatibility — a target's `Accepts`\ntakes the drag if it shares at least one tag. Everything else is yours to\nshape (guids, indices, a display name for your ghost, ...).\n\nBecause `Tags` travels with the payload and `GetPayload` runs on every lift, a\nsource whose contents vary can return different tags per lift — e.g. a bag\nslot returns `{ \"Item\", \"Food\" }` for a ration and `{ \"Item\", \"Weapon\" }` for\na sword, and each matches the right target.",
            "lua_type": "{ Tags: { string }, [string]: any }",
            "source": {
                "line": 35,
                "path": "lib/dragdrop/src/Types.luau"
            }
        },
        {
            "name": "TargetDescriptor",
            "desc": "Handed to a target's `OnDrop` and returned by [DragDrop.GetHoveredTarget].",
            "fields": [
                {
                    "name": "Instance",
                    "lua_type": "GuiObject",
                    "desc": "The registered target the drop resolved onto."
                },
                {
                    "name": "Metadata",
                    "lua_type": "{ [any]: any }",
                    "desc": "Whatever `Metadata` the target registered with."
                }
            ],
            "source": {
                "line": 48,
                "path": "lib/dragdrop/src/Types.luau"
            }
        },
        {
            "name": "SourceOptions",
            "desc": "",
            "fields": [
                {
                    "name": "GetPayload",
                    "lua_type": "() -> Payload?",
                    "desc": "Returns the payload to lift, or nil to refuse the lift (empty slot, disabled, ...). Called on press."
                },
                {
                    "name": "OnActivated",
                    "lua_type": "(() -> ())?",
                    "desc": "Press + release without crossing the drag threshold (a \"click\")."
                },
                {
                    "name": "OnLift",
                    "lua_type": "((payload: Payload) -> ())?",
                    "desc": "Fired the moment the payload goes airborne."
                },
                {
                    "name": "OnDragEnd",
                    "lua_type": "((payload: Payload, dropped: boolean) -> ())?",
                    "desc": "Fired on every drag end. `dropped=false` means cancelled/reverted."
                },
                {
                    "name": "BuildGhost",
                    "lua_type": "((payload: Payload, source: GuiObject) -> GuiObject)?",
                    "desc": "Builds the dragged proxy. Strongly recommended; nil falls back to a bare debug plate."
                }
            ],
            "source": {
                "line": 62,
                "path": "lib/dragdrop/src/Types.luau"
            }
        },
        {
            "name": "TargetOptions",
            "desc": "",
            "fields": [
                {
                    "name": "Accepts",
                    "lua_type": "{ string }?",
                    "desc": "Tags this target accepts; matches if it shares at least one tag with the payload's `Tags`. Empty or omitted accepts any payload."
                },
                {
                    "name": "CanDrop",
                    "lua_type": "((payload: Payload) -> boolean)?",
                    "desc": "Optional finer gate, checked in addition to `Accepts`."
                },
                {
                    "name": "OnDrop",
                    "lua_type": "(payload: Payload, target: TargetDescriptor) -> ()",
                    "desc": "Called when a compatible payload is dropped here."
                },
                {
                    "name": "OnHoverChanged",
                    "lua_type": "((payload: Payload?) -> ())?",
                    "desc": "`payload` on enter, nil on leave."
                },
                {
                    "name": "Metadata",
                    "lua_type": "{ [any]: any }?",
                    "desc": "Arbitrary data surfaced back through `TargetDescriptor.Metadata`."
                }
            ],
            "source": {
                "line": 84,
                "path": "lib/dragdrop/src/Types.luau"
            }
        },
        {
            "name": "InputMode",
            "desc": "`Automatic` (default) wires the pointer and selection backends to real device\ninput. `Scriptable` disables them so the only way a drag starts or moves is\nthrough [DragDrop.BeginDrag] and the returned [DragHandle] — like putting a\ncamera in Scriptable mode.",
            "lua_type": "\"Automatic\" | \"Scriptable\"",
            "source": {
                "line": 101,
                "path": "lib/dragdrop/src/Types.luau"
            }
        },
        {
            "name": "Config",
            "desc": "Behavior knobs only — nothing here styles the ghost (that lives in `BuildGhost`).",
            "fields": [
                {
                    "name": "DragThresholdPx",
                    "lua_type": "number?",
                    "desc": "Mouse: movement past this (in pixels) lifts. Default 8."
                },
                {
                    "name": "TouchSlopPx",
                    "lua_type": "number?",
                    "desc": "Touch: movement past this before the hold fires = scroll, not lift. Default 12."
                },
                {
                    "name": "LongPressSeconds",
                    "lua_type": "number?",
                    "desc": "Touch: hold duration that lifts. Default 0.4."
                },
                {
                    "name": "SnapSpeed",
                    "lua_type": "number?",
                    "desc": "Selection mode: slot-to-slot ghost spring speed. Default 30."
                },
                {
                    "name": "GhostContainer",
                    "lua_type": "LayerCollector?",
                    "desc": "Parent for ghost mounts. nil = an internal high-DisplayOrder ScreenGui."
                },
                {
                    "name": "GamepadLiftKeyCode",
                    "lua_type": "Enum.KeyCode?",
                    "desc": "Button that lifts the selected source. Default ButtonX."
                },
                {
                    "name": "GamepadDropKeyCode",
                    "lua_type": "Enum.KeyCode?",
                    "desc": "Button that drops on the selected target. Default ButtonA."
                },
                {
                    "name": "GamepadCancelKeyCode",
                    "lua_type": "Enum.KeyCode?",
                    "desc": "Button that cancels a drag. Default ButtonB."
                },
                {
                    "name": "KeyboardLiftKeyCode",
                    "lua_type": "Enum.KeyCode?",
                    "desc": "Key that toggles lift/drop in grab mode. Default Return."
                },
                {
                    "name": "KeyboardCancelKeyCode",
                    "lua_type": "Enum.KeyCode?",
                    "desc": "Optional explicit keyboard cancel key. Default nil."
                }
            ],
            "source": {
                "line": 119,
                "path": "lib/dragdrop/src/Types.luau"
            }
        },
        {
            "name": "DragHandle",
            "desc": "A programmatic controller for a single drag, returned by [DragDrop.BeginDrag].\nEvery method becomes a no-op (and the mutating ones return false) once the\ndrag has ended.",
            "fields": [
                {
                    "name": "MoveTo",
                    "lua_type": "(self: DragHandle, position: Vector2) -> ()",
                    "desc": "Move the ghost to a screen point and hit-test for hover."
                },
                {
                    "name": "SetHoveredTarget",
                    "lua_type": "(self: DragHandle, target: GuiObject?) -> boolean",
                    "desc": "Set hover explicitly; false if the target is invalid/incompatible."
                },
                {
                    "name": "Drop",
                    "lua_type": "(self: DragHandle) -> boolean",
                    "desc": "End on the current hover; false if there was no valid target (reverted)."
                },
                {
                    "name": "Cancel",
                    "lua_type": "(self: DragHandle) -> ()",
                    "desc": "Revert this drag."
                },
                {
                    "name": "IsActive",
                    "lua_type": "(self: DragHandle) -> boolean",
                    "desc": "Whether this handle still controls the live drag."
                },
                {
                    "name": "GetPayload",
                    "lua_type": "(self: DragHandle) -> Payload",
                    "desc": "The payload this drag carries."
                }
            ],
            "source": {
                "line": 146,
                "path": "lib/dragdrop/src/Types.luau"
            }
        },
        {
            "name": "Unregister",
            "desc": "Returned by [DragDrop.RegisterSource] and [DragDrop.RegisterTarget]. Call it to\ntear the registration down.",
            "lua_type": "() -> ()",
            "source": {
                "line": 162,
                "path": "lib/dragdrop/src/Types.luau"
            }
        }
    ],
    "name": "DragDrop",
    "desc": "Device-agnostic drag-and-drop for Roblox UI. A singleton that moves an opaque\n[Payload] from a registered source to a registered target across **mouse,\ntouch, gamepad, and keyboard**, emitting intent through the target's `OnDrop`.\nIt never inspects a payload past its `Tags`, so one registration handles any\nkind of content.\n\nRegister a source and/or a target on a `GuiObject` and keep the returned\n[Unregister] alive for as long as the element should participate. In a\ncomponent framework, insert it into your cleanup scope:\n\n```lua\nlocal DragDrop = require(Packages.DragDrop)\n\nlocal disconnectSource = DragDrop.RegisterSource(slot, {\n\tGetPayload = function()\n\t\treturn { Tags = { \"Item\" }, Guid = guid, FromIndex = i }\n\tend,\n\tBuildGhost = function(payload, source) return MyItemGhost(payload) end,\n\tOnActivated = openContextMenu, -- click / A-press without a lift\n})\n\nlocal disconnectTarget = DragDrop.RegisterTarget(slot, {\n\tAccepts = { \"Item\" }, -- only takes item drags\n\tOnDrop = function(payload, target) MoveItem(payload, target) end,\n\tMetadata = { Index = i },\n})\n```\n\n### Where to read more\n\n- **[DD Getting Started](/api/DD%20Getting%20Started)** — registering sources and\n  targets, how `Tags`/`Accepts`/`CanDrop` decide compatibility, and reacting to\n  drag state.\n- **[DD Input Devices](/api/DD%20Input%20Devices)** — the gesture per device, the\n  configurable thresholds and keybinds, and what switching input class mid-drag\n  does.\n- **[DD Ghosts & Scripting](/api/DD%20Ghosts%20&%20Scripting)** — customizing the\n  drag proxy with `BuildGhost`, and driving drags by hand through\n  [DragDrop.SetInputMode] and [DragHandle].",
    "source": {
        "line": 45,
        "path": "lib/dragdrop/src/DragDrop.luau"
    }
}