See the question and my original answer on StackOverflow

It's because when running .NET you're living in a "managed" world, and objects can be moved in memory when .NET needs to, typically when the garbage collector runs. So what happens is you pass a pointer to WinUI3 code and that pointer goes rogue at some moment and you get a big crash.

So you must "pin" the objects you pass to unmanaged memory so they won't be moved. The easiest way is to create a static variable, something like this:

public sealed partial class MainWindow : Window
{
    private static WNDPROC? wndProc;
    private static WNDPROC hp = HotKeyProc; // fixes method pointer
    private static MainWindow? _window;

    private static LRESULT HotKeyProc(HWND hWnd, uint Msg, WPARAM wParam, LPARAM lParam)
    {
        const uint WM_HOTKEY = 0x0312; // HotKey Window Message

        if (Msg == WM_HOTKEY)
        {
            if (_window?.ToggleButton.IsEnabled == true)
            {
                _window.ToggleButton.IsChecked = !_window.ToggleButton.IsChecked;
            }
        }

        return PInvoke.CallWindowProc(wndProc, hWnd, Msg, wParam, lParam);
    }

    public MainWindow()
    {
        this.InitializeComponent();

        _window = this;

        // Get window handle
        HWND hWnd = new(WinRT.Interop.WindowNative.GetWindowHandle(this));

        // Register hotkey
        int id = 0x0000;
        _ = PInvoke.RegisterHotKey(hWnd, id, HOT_KEY_MODIFIERS.MOD_NOREPEAT, 0x75); // F6

        // Add hotkey function pointer to window procedure
        nint hotKeyProcPtr = Marshal.GetFunctionPointerForDelegate(hp);
        nint wndPtr = PInvoke.SetWindowLongPtr(hWnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyProcPtr);
        wndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(wndPtr);
    }
}

There is a example here How to make a global keyboard accelerator/hotkey for a button? that has some reusable code and works a bit differently