PlayerInteract.cs
using Sandbox;
using Sandbox.UI;
using System;
using System.Runtime.CompilerServices;

public sealed class PlayerInteract : Component
{
	[Property] public string ActionName { get; set; } = "Use";
	[Property] public string TagName { get; set; } = "interact";
	[Property] public float RayLength { get; set; } = 125.0f;
	[Property] public float InteractCooldown { get; set; } = 2.0f;
	public event Action<SceneTraceResult> OnInteract;
	public event Action<SceneTraceResult> OnCanInteract;
	public event Action<SceneTraceResult> OnCanInteractEnd;
	private CameraComponent _camera;
	private TimeSince _timeSince;
	private bool _prevCanInteract;

	protected override void OnStart()
	{
		base.OnStart();

		_camera = GetComponentInChildren<PlayerController>().UseCameraControls ? Scene.Camera : GetComponentInChildren<CameraComponent>();
		if ( _camera == null )
		{
			Log.Error( "Failed to get CameraComponent" );
			return;
		}

		_timeSince = InteractCooldown; // allow first interact instantly
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();

		UpdateInteract();
	}

	[MethodImpl( MethodImplOptions.AggressiveInlining )]
	public bool CanInteract( SceneTraceResult trace ) => 
		_timeSince > InteractCooldown && trace.Hit && trace.GameObject.Tags.Has( TagName );

	public void Interact( SceneTraceResult trace )
	{
		_timeSince = 0;
		OnInteract?.Invoke( trace );
	}

	private void UpdateInteract()
	{
		SceneTraceResult trace = CastRay();
		bool canInteract = CanInteract( trace );

		if ( canInteract != _prevCanInteract )
		{
			if ( canInteract )
				OnCanInteract?.Invoke( trace );
			else
				OnCanInteractEnd?.Invoke( trace );

			_prevCanInteract = canInteract;
		}

		if ( canInteract && Input.Released( ActionName ) )
		{
			Interact( trace );
		}
	}

	private SceneTraceResult CastRay()
	{
		Vector3 direction = ( _camera.ScreenToWorld( Screen.Size * 0.5f ) - _camera.WorldPosition ).Normal;
		Vector3 start = GameObject.WorldPosition + new Vector3( 0.0f, 0.0f, 64.0f );
		Vector3 end = start + direction * RayLength;

		return Scene.Trace.Ray( start, end ).IgnoreGameObjectHierarchy( GameObject ).Run();
	}
}