Conveyance/InteractionController.cs
using Sandbox;

/// <summary>
/// Interface implemented by anything in the world a player can interact with.
/// Add this to any component (InteractButton, future levers, pickups, etc.)
/// </summary>
public interface IInteractable
{
	string InteractionText { get; }
	void OnInteract( GameObject interactor );
}

/// <summary>
/// Attach to the player root alongside Player.
/// Uses Player.EyeTransform for the raycast — consistent with how Physgun
/// and ToolGun do their traces in this codebase.
/// </summary>
public sealed class InteractionController : Component
{
	[Property] public float MaxDistance { get; set; } = 120f;

	/// <summary>Currently looked-at interactable, or null. Read by GameHUD for prompt.</summary>
	public IInteractable Current { get; private set; }

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

		Current = null;

		// Use the sandbox-main Player component, same as Physgun/ToolGun do.
		var player = Components.Get<Player>();
		if ( player == null ) return;

		var eye = player.EyeTransform;
		var tr  = Scene.Trace
			.Ray( eye.Position, eye.Position + eye.Rotation.Forward * MaxDistance )
			.IgnoreGameObjectHierarchy( GameObject )
			.Run();

		if ( tr.Hit && tr.GameObject != null )
			Current = tr.GameObject.Components.Get<IInteractable>( true );

		if ( Current != null && Input.Pressed( "Use" ) )
			Current.OnInteract( GameObject );
	}
}