DesktopGrab.cs
using Sandbox;
using Sandbox.Citizen;
using Sandbox.Mapping;
using System.Linq;

public sealed class DesktopGrab : Component
{
	[RequireComponent] private PlayerViewModel PlayerViewModel { get; set; }
	[Property] private CameraComponent Camera { get; set; }
	[Property] public float InteractionDistance { get; set; } = 150f;

	private Player Player { get; set; }
	private PlayerInventory Inventory { get; set; }
	private PlayerAnimationBroadcast AnimBroadcaster { get; set; }

	private GrabPoint _activeGrabPoint;
	private GrabPoint _secondaryGrabPoint;

	private bool _throwWindupActive;
	private bool _throwRequested;
	private float _throwReadyAt;

	private bool _throwAnimActive;
	private float _throwReleaseAt;

	private static readonly string[] SlotActions = ["slot1", "slot2", "slot3", "slot4", "slot5"];

	protected override void OnStart()
	{
		Player = GetComponentInParent<Player>();
		Inventory = Player?.Inventory;
		AnimBroadcaster = Player?.Avatar?.GetComponent<PlayerAnimationBroadcast>();

		var initialInventorySlot = Inventory?.Slots.FirstOrDefault( slot => slot.StoredItem.IsValid() );
		if ( initialInventorySlot.IsValid() )
		{
			EquipFromSlot( initialInventorySlot );
		}
	}

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

		// The held item may have been destroyed out from under us (e.g. a grenade
		// exploding in hand) — clean up the stale state before doing anything else.
		if ( _activeGrabPoint is not null && !_activeGrabPoint.IsValid() )
			ClearHeldState();

		if ( _throwAnimActive && Time.Now >= _throwReleaseAt )
		{
			ReleaseToWorld( GetThrowVelocity() );
			ClearThrowAnimState();
			return;
		}

		HandleSlotInput();

		if ( Input.Pressed( "use" ) )
			HandleUseInput();

		if ( Input.Pressed( "drop" ) )
			ForceDropHeld();

		if ( _activeGrabPoint is not null && !_throwAnimActive )
		{
			var throwable = _activeGrabPoint.ItemRoot.GetComponent<Throwable>();
			var isThrowable = throwable is not null;

			if ( Input.Pressed( "attack1" ) && isThrowable )
			{
				PlayerViewModel.Instance.PlayCharge();
				StartThrowWindup( throwable.ThrowDelay );
			}

			if ( Input.Released( "attack1" ) && _throwWindupActive )
				_throwRequested = true;

			if ( _throwWindupActive && _throwRequested && Time.Now >= _throwReadyAt )
				BeginThrowAnimation( throwable );

			_activeGrabPoint.SetTriggerState( Input.Down( "attack1" ) );

			if ( Input.Pressed( "reload" ) )
				_activeGrabPoint.Reload();
		}
	}

	private void HandleUseInput()
	{
		var gameObject = GetLookTrace();

		if ( !gameObject.IsValid() )
			return;

		var door = gameObject.GetComponent<Door>();
		if ( door.IsValid() )
			door.Toggle( gameObject );
		else
			TryGrabObject( gameObject );
	}

	private void HandleSlotInput()
	{
		for ( int i = 0; i < SlotActions.Length; i++ )
		{
			if ( !Input.Pressed( SlotActions[i] ) )
				continue;

			HandleSlotKey( (InventorySlotIndex)i );
		}
	}

	private void HandleSlotKey( InventorySlotIndex slotIndex )
	{
		if ( Inventory is null )
			return;

		var slot = Inventory.GetSlot( slotIndex );
		if ( slot is null )
			return;

		var slotItem = slot.StoredItem;
		if ( slotItem is null )
			return;

		var currentItem = _activeGrabPoint?.Components.GetInAncestorsOrSelf<InventoryItem>();
		bool pressedHeldItemsSlot = currentItem is not null && currentItem.SlotObject == slot.GameObject;

		if ( !HolsterCurrentItem() )
			return;

		// Tapping the held item's own slot key just holsters it
		if ( pressedHeldItemsSlot )
			return;

		EquipFromSlot( slot );
	}

	private GameObject GetLookTrace()
	{
		var ray = Camera.ScreenNormalToRay( new Vector2( 0.5f, 0.5f ) );

		var trace = Scene.Trace.Ray( ray, InteractionDistance )
			.UsePhysicsWorld()
			.Run();

		if ( !trace.Hit || trace.GameObject is null )
			return null;

		return trace.GameObject;
	}

	private void TryGrabObject( GameObject gameObject )
	{
		var itemRoot = gameObject.Root;
		var inventoryItem = itemRoot.Components.Get<InventoryItem>();
		if ( inventoryItem is null || Inventory is null || inventoryItem.IsStowed )
			return;

		var freeSlot = Inventory.TryGetFreeSlot( inventoryItem.Type );
		if ( freeSlot is null )
			return;

		if ( !CanEquip( inventoryItem ) || !HolsterCurrentItem() )
			return;

		// Equip first: Grab() takes network ownership of the item, and only the
		// owner may write the item's [Sync] slot properties in Reserve().
		EquipToHand( inventoryItem );
		freeSlot.Reserve( inventoryItem );
	}

	private void EquipFromSlot( InventorySlot slot )
	{
		var inventoryItem = slot.StoredItem;
		if ( inventoryItem is null || !CanEquip( inventoryItem ) )
			return;

		EquipToHand( inventoryItem );
	}

	/// <summary>True when the item has a primary grip that no one is holding yet.</summary>
	private static bool CanEquip( InventoryItem item )
	{
		var primaryGrip = FindGrip( item, secondary: false );

		return primaryGrip is not null && !primaryGrip.IsGrabbed;
	}

	private static GrabPoint FindGrip( InventoryItem item, bool secondary )
	{
		return item.GameObject.Components
			.GetAll<GrabPoint>( FindMode.InDescendants )
			.FirstOrDefault( g => g.IsSecondaryGrip == secondary );
	}

	private void EquipToHand( InventoryItem item )
	{
		_activeGrabPoint = FindGrip( item, secondary: false );
		_secondaryGrabPoint = FindGrip( item, secondary: true );

		// Unstows the item and takes network ownership of it
		_activeGrabPoint.Grab( false );

		var weaponRoot = item.GameObject;
		var handRAttachment = Player.Avatar.GetAttachmentObject( "hold_r" );
		weaponRoot.SetParent( handRAttachment, false );

		weaponRoot.LocalRotation = Rotation.FromRoll( 0 );
		weaponRoot.LocalPosition = item.RightHandPosition;

		AnimBroadcaster.HoldType = item.HoldType;
		AnimBroadcaster.Handedness = item.Handedness;
		AnimBroadcaster.SecondaryGrip = _secondaryGrabPoint?.GameObject;

		// Desktop holds show the viewmodel instead of the world model
		item.SetWorldModelVisible( false );

		var viewModel = _activeGrabPoint.GetComponentInParent<ViewModel>();
		if ( viewModel is not null )
			PlayerViewModel.Show( viewModel );
	}

	/// <summary>
	/// Puts the held item back in its inventory slot, keeping it on the body.
	/// Returns true if the hand ended up empty.
	/// </summary>
	private bool HolsterCurrentItem()
	{
		if ( _activeGrabPoint is null )
			return true;

		var inventoryItem = _activeGrabPoint.Components.GetInAncestorsOrSelf<InventoryItem>();
		if ( inventoryItem is null || Inventory is null || !Inventory.TryStow( inventoryItem ) )
			return false;

		_activeGrabPoint.Drop();
		ClearHeldState();

		return true;
	}

	/// <summary>Drops the held item into the world, ignoring its inventory slot.</summary>
	public void ForceDropHeld()
	{
		ReleaseToWorld( null );
	}

	/// <summary>
	/// Throws the held item along the camera direction using its Throwable settings.
	/// </summary>
	public void ThrowObject()
	{
		if ( _activeGrabPoint is null )
			return;

		var throwable = _activeGrabPoint.ItemRoot.GetComponent<Throwable>();
		if ( throwable is null )
			return;

		BeginThrowAnimation( throwable );
	}

	/// <summary>
	/// Plays the throw viewmodel animation, then releases the item after ThrowAnimDuration.
	/// </summary>
	private void BeginThrowAnimation( Throwable throwable )
	{
		if ( _activeGrabPoint is null )
			return;

		ClearThrowWindup();

		PlayerViewModel.Instance?.PlayAttack();

		_throwAnimActive = true;
		_throwReleaseAt = Time.Now + throwable.ThrowAnimDuration;
	}

	private Vector3? GetThrowVelocity()
	{
		if ( _activeGrabPoint is null )
			return null;

		var throwable = _activeGrabPoint.ItemRoot.GetComponent<Throwable>();
		if ( throwable is null )
			return null;

		var ray = Camera.ScreenNormalToRay( new Vector2( 0.5f, 0.5f ) );
		var direction = (ray.Forward + Vector3.Up * throwable.UpwardBoost).Normal;

		return direction * throwable.ThrowForce;
	}

	private void ReleaseToWorld( Vector3? throwVelocity )
	{
		if ( _activeGrabPoint is null )
			return;

		_activeGrabPoint.ItemRoot.Components.Get<InventoryItem>()?.DropToWorld();
		_activeGrabPoint.Drop( throwVelocity );

		ClearHeldState();
	}

	/// <summary>
	/// Forgets the currently held item and resets viewmodel/animation state.
	/// Safe to call when the item has already been destroyed.
	/// </summary>
	private void ClearHeldState()
	{
		PlayerViewModel.Hide();

		_activeGrabPoint = null;
		_secondaryGrabPoint = null;

		ClearThrowWindup();
		ClearThrowAnimState();

		AnimBroadcaster.HoldType = CitizenAnimationHelper.HoldTypes.None;
		AnimBroadcaster.SecondaryGrip = null;
	}

	private void StartThrowWindup( float delay )
	{
		_throwWindupActive = true;
		_throwRequested = false;
		_throwReadyAt = Time.Now + delay;
	}

	private void ClearThrowWindup()
	{
		_throwWindupActive = false;
		_throwRequested = false;
		_throwReadyAt = 0f;
	}

	private void ClearThrowAnimState()
	{
		_throwAnimActive = false;
		_throwReleaseAt = 0f;
	}
}