Code/PlayerPickupHandler.cs
using Sandbox;

/// <summary>
/// Add to your player GameObject alongside InventoryComponent.
/// Handles the interact key (default "use") to pick up ItemPickup objects,
/// open StorageContainers, and open CraftingStations via raycasts.
/// </summary>
public sealed class PlayerPickupHandler : Component
{
    /// <summary>Maximum interaction distance in world units.</summary>
    [Property] public float MaxDistance { get; set; } = 200f;

    /// <summary>Radius of the interaction trace sphere.</summary>
    [Property] public float TraceRadius { get; set; } = 12f;

    /// <summary>Eye height offset from the player's transform position.</summary>
    [Property] public float EyeHeight { get; set; } = 56f;

    /// <summary>Vertical pixel offset applied to the screen-center ray. Use to compensate for camera positioning.</summary>
    [Property] public float AimOffsetY { get; set; } = 0f;

    /// <summary>Input action name that triggers interaction. Defaults to "use" (E key in default S&box bindings).</summary>
    [Property] public string InteractInput { get; set; } = "use";

    protected override void OnUpdate()
    {
        if ( !Input.Pressed( InteractInput ) ) return;

        var inventory = Components.Get<InventoryComponent>();
        if ( inventory == null ) return;

        var cam = Scene.Camera;
        if ( cam == null ) return;

        // Trace from camera through screen center (aligns with crosshair)
        var screenCenter = new Vector2( Screen.Width * 0.5f, Screen.Height * 0.5f + AimOffsetY );
        var camRay = cam.ScreenPixelToRay( screenCenter );
        var tr = Scene.Trace.Ray( camRay, MaxDistance )
            .Radius( TraceRadius )
            .IgnoreGameObjectHierarchy( GameObject )
            .Run();

        if ( tr.Hit && tr.GameObject != null )
        {
            var pickup = tr.GameObject.Components.Get<ItemPickup>();
            if ( pickup != null && pickup.Pickup( inventory ) )
                return;

            var container = tr.GameObject.Components.Get<StorageContainer>();
            if ( container != null )
            {
                OpenContainer( container );
                return;
            }

            var station = tr.GameObject.Components.Get<CraftingStation>();
            if ( station != null )
            {
                CraftingStation.OnStationOpened?.Invoke( station );
                return;
            }
        }

        // Fallback: trace from player's eye position in the camera's forward direction
        var eyePos = Transform.Position + Vector3.Up * EyeHeight;
        var tr2 = Scene.Trace.Ray( eyePos, eyePos + camRay.Forward * MaxDistance )
            .Radius( TraceRadius )
            .IgnoreGameObjectHierarchy( GameObject )
            .Run();

        if ( tr2.Hit && tr2.GameObject != null )
        {
            var pickup2 = tr2.GameObject.Components.Get<ItemPickup>();
            if ( pickup2 != null )
            {
                pickup2.Pickup( inventory );
                return;
            }

            var container2 = tr2.GameObject.Components.Get<StorageContainer>();
            if ( container2 != null )
            {
                OpenContainer( container2 );
                return;
            }

            var station2 = tr2.GameObject.Components.Get<CraftingStation>();
            if ( station2 != null )
                CraftingStation.OnStationOpened?.Invoke( station2 );
        }
    }

    private void OpenContainer( StorageContainer container )
    {
        StorageContainer.OnOpenRequested?.Invoke( container );
    }
}