HandGrab.cs
using Sandbox;
using Sandbox.Mapping;
using Sandbox.VR;
using System;
using System.Linq;

public sealed class HandGrab : Component
{
	[Property] public bool IsLeftHand { get; set; } = false;

	/// <summary>Grip axis value (0-1) needed to pick up. Keep low for a light hold; must stay above Release Threshold.</summary>
	[Property, Group( "Grip" )]
	public float GripGrabThreshold { get; set; } = 0.10f;

	/// <summary>Grip axis value (0-1) at or below which the held item is released. Keep below Grab Threshold so throws aren't re-caught.</summary>
	[Property, Group( "Grip" )]
	public float GripReleaseThreshold { get; set; } = 0f;

	private const float GrabSphereRadius = 3.5f;
	private const float GrabReachDistance = 10f;
	private const float PalmOverlapRadius = 4f;

	private GrabPoint _activeGrabPoint;

	// Helpers to automatically pull input from the correct physical controller
	private VRController ControllerInput => IsLeftHand ? Input.VR.LeftHand : Input.VR.RightHand;
	private Transform HandTransform => ControllerInput.Transform;

	private bool _useDown = false;
	private bool _reloadDown = false;

	/// <summary>
	/// True after grip has fallen to the release threshold. Prevents immediately
	/// re-grabbing a thrown item while the grip axis is still near zero.
	/// </summary>
	private bool _gripReadyToGrab = true;

	protected override void OnUpdate()
	{
		if ( IsProxy )
			return;

		if ( Player.CurrentPlayer.IsValid() && Player.CurrentPlayer.Health.IsDead )
		{
			ForceDropHeld();
			return;
		}

		HandleUseInput();
		HandleGrabInput();
		HandleActionInput();
		HandleReloadInput();
	}

	private void HandleUseInput()
	{
		var triggerIsPressed = ControllerInput.Trigger.Value >= 0.75f;

		if ( triggerIsPressed && !_useDown )
		{
			var handPosition = HandTransform.Position;
			var handRotation = HandTransform.Rotation;

			float reachDistance = 20f;
			var angles = handRotation.Angles();
			angles.pitch += 60f;
			var forwardDirection = Rotation.From( angles ).Forward;
			var endPosition = handPosition + (forwardDirection * reachDistance);
			var trace = Scene.Trace.Ray( handPosition, endPosition ).Run();

			var door = trace.GameObject?.GetComponent<Door>();
			door?.Toggle( Player.CurrentPlayer?.GameObject );
		}

		_useDown = triggerIsPressed;
	}

	private void HandleGrabInput()
	{
		var grip = ControllerInput.Grip.Value;
		// Never allow grab <= release, even if inspector values are inverted/zeroed
		float releaseThreshold = Math.Clamp( GripReleaseThreshold, 0f, 0.95f );
		float grabThreshold = Math.Max( GripGrabThreshold, releaseThreshold + 0.02f );

		if ( grip <= releaseThreshold )
			_gripReadyToGrab = true;

		// Require a fresh squeeze after a full release so drops/throws aren't
		// instantly re-caught by palm proximity while grip is still low.
		if ( grip >= grabThreshold && _activeGrabPoint is null && _gripReadyToGrab )
		{
			TryGrabNearestObject();
			if ( _activeGrabPoint is not null )
				_gripReadyToGrab = false;
		}

		// Drop only after grip falls below the release threshold
		if ( grip <= releaseThreshold && _activeGrabPoint is not null )
		{
			TryStowOrDrop();
			_gripReadyToGrab = true;
		}
	}

	/// <summary>
	/// Stows the held item if the hand let go inside one of the player's slots,
	/// otherwise releases it into the world.
	/// </summary>
	private void TryStowOrDrop()
	{
		if ( _activeGrabPoint is null )
			return;

		var inventoryItem = _activeGrabPoint.Components.GetInAncestorsOrSelf<InventoryItem>();
		var inventory = Player.CurrentPlayer?.Inventory;

		if ( inventoryItem is not null && inventory is not null && inventory.TryStow( inventoryItem, HandTransform.Position ) )
		{
			// The item stays parented to the slot, so it must not fall to the world
			_activeGrabPoint.Drop();
			_activeGrabPoint = null;
			return;
		}

		ForceDropHeld();
	}

	private void HandleActionInput()
	{
		if ( _activeGrabPoint is null ) return;

		// Report the index trigger state each frame so the item can decide how to
		// respond (continuous fire vs. once-per-press toggles)
		_activeGrabPoint.SetTriggerState( ControllerInput.Trigger.Value > 0.75f );
	}

	private void HandleReloadInput()
	{
		if ( ControllerInput.ButtonB.IsPressed && !_reloadDown )
		{
			_activeGrabPoint?.Reload();
		}

		_reloadDown = ControllerInput.ButtonB.IsPressed;
	}

	private void TryGrabNearestObject()
	{
		var handPosition = HandTransform.Position;
		var handRotation = HandTransform.Rotation;

		var angles = handRotation.Angles();
		angles.pitch += 60f;
		var forwardDirection = Rotation.From( angles ).Forward;
		var endPosition = handPosition + (forwardDirection * GrabReachDistance);

		// Reach cast along the pointed palm, plus a palm-centered overlap for nearby grips
		var reachHits = Scene.Trace.Sphere( GrabSphereRadius, handPosition, endPosition ).HitTriggersOnly().RunAll();
		var palmHits = Scene.Trace.Sphere( PalmOverlapRadius, handPosition, handPosition ).HitTriggersOnly().RunAll();

		var closestGrabPoint = reachHits.Concat( palmHits )
			.Select( hit => hit.Collider?.Components.Get<GrabPoint>() )
			.Where( grabPoint => grabPoint is not null && !grabPoint.IsGrabbed )
			.Distinct()
			.OrderBy( grabPoint => handPosition.Distance( grabPoint.WorldPosition ) )
			.FirstOrDefault();

		if ( closestGrabPoint is not null )
		{
			// Grab() unstows the item; unlike desktop the slot isn't kept reserved, so the
			// hand is free to stow it in any slot it reaches later on.
			_activeGrabPoint = closestGrabPoint;
			_activeGrabPoint.Grab( IsLeftHand );
			_activeGrabPoint.Components.GetInAncestorsOrSelf<InventoryItem>()?.ClearSlot();
		}
	}

	/// <summary>Releases the held item, stowing it if the hand is at a matching slot.</summary>
	public void DropHeld() => TryStowOrDrop();

	/// <summary>Drops the held item into the world, ignoring nearby inventory slots.</summary>
	public void ForceDropHeld()
	{
		if ( _activeGrabPoint is null )
			return;

		_activeGrabPoint.Components.GetInAncestorsOrSelf<InventoryItem>()?.DropToWorld();
		_activeGrabPoint.Drop();
		_activeGrabPoint = null;
	}
}