VehicleCamera.cs
using Sandbox;

namespace Bugges.VehicleController;

[Icon( "local_taxi" )]
public sealed class VehicleCamera : Component
{
	[Property]
	private GameObject Target { get; set; }

	[Property]
	private float DefaultDistance { get; set; } = 500.0f;

	[Property]
	private Vector2 DistanceRange { get; set; } = new( 200.0f, 800.0f );

	[Property]
	private bool IsOffsetLocalSpace { get; set; } = false;

	[Property]
	private Vector3 Offset { get; set; }

	[FeatureEnabled( "Auto Center" ), Property]
	private bool ShouldAutoCenter { get; set; } = false;

	[Feature( "Auto Center" ), Property]
	private float AutoCenterDelay { get; set; } = 1.5f;

	[Feature( "Auto Center" ), Property]
	private float AutoCenterSpeed { get; set; } = 4.0f;

	[Feature( "Auto Center" ), Property]
	private float DefaultPitch { get; set; } = 15.0f;

	[ReadOnly, Property]
	private float CurrentDistance { get; set; }

	private Angles _eyeAngles;
	private TimeSince _timeSinceLastLook;


	protected override void OnStart()
	{
		if ( IsProxy )
		{
			DestroyGameObject();
			return;
		}

		CurrentDistance = DefaultDistance;
		_eyeAngles = WorldRotation.Angles();
	}

	protected override void OnUpdate()
	{
		if ( Target is null ) return;

		Zoom();
		AutoCenter();
		_eyeAngles.pitch = float.Clamp( _eyeAngles.pitch, -89f, 89f );
		KeepDistance();
	}

	private void KeepDistance()
	{
		_eyeAngles.roll = 0f;
		Rotation rotation = _eyeAngles.ToRotation();
		Vector3 targetPosition = GetTargetPosition();
		Vector3 backwardDir = -rotation.Forward;

		var ray = new Ray( targetPosition, backwardDir );
		var trace = Scene.Trace
			.Sphere( 10f, ray, CurrentDistance )
			.IgnoreGameObject( Target )
			.IgnoreDynamic()
			.Run();

		float targetDist = trace.Hit ? trace.Distance : CurrentDistance;

		WorldPosition = targetPosition + backwardDir * targetDist;
		WorldRotation = rotation;
	}

	private void Zoom()
	{
		CurrentDistance -= Input.MouseWheel.y * 100.0f;
		CurrentDistance = float.Clamp( CurrentDistance, DistanceRange.x, DistanceRange.y );
	}

	private void AutoCenter()
	{
		Angles lookInput = Input.AnalogLook;
		if ( lookInput.pitch != 0f || lookInput.yaw != 0f )
		{
			_timeSinceLastLook = 0;
			_eyeAngles += lookInput;
			return;
		}

		if ( !ShouldAutoCenter ) return;
		if ( _timeSinceLastLook <= AutoCenterDelay ) return;

		var rb = Target.GetComponent<Rigidbody>();
		if ( rb is not null && rb.Velocity.Length < 1.0f ) return;

		Rotation targetRot = Rotation.From( DefaultPitch, Target.WorldRotation.Yaw(), 0f );
		Rotation currentRot = _eyeAngles.ToRotation();

		currentRot = Rotation.Slerp( currentRot, targetRot, Time.Delta * AutoCenterSpeed );

		_eyeAngles = currentRot.Angles();
	}

	private Vector3 GetTargetPosition()
	{
		Vector3 worldSpaceOffset = Target.WorldTransform.Right * Offset.x + Target.WorldTransform.Forward * Offset.y + Target.WorldTransform.Up * Offset.z;
		return Target.WorldPosition + (IsOffsetLocalSpace ? worldSpaceOffset : Offset);
	}
}