Scroll to Nearest Paragraph (← → keys)

Scroll to nearest paragraph using ← and → keys with 50px offset, starting from current scroll position if already scrolled down the page (no modifier keys required)

// ==UserScript==
// @name         Scroll to Nearest Paragraph (← → keys)
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Scroll to nearest paragraph using ← and → keys with 50px offset, starting from current scroll position if already scrolled down the page (no modifier keys required)
// @author       Işık Barış Fidaner
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    let paragraphs = [];
    let current = 0;
    let initialized = false;
    const offset = 50; // pixels above paragraph

    function init() {
        paragraphs = Array.from(document.querySelectorAll('p'))
            .filter(p => p.offsetHeight > 0 && p.offsetParent !== null);
        setInitialIndex();
        initialized = true;
    }

    function setInitialIndex() {
        const viewportTop = window.scrollY;
        for (let i = 0; i < paragraphs.length; i++) {
            const rect = paragraphs[i].getBoundingClientRect();
            const paragraphTop = window.scrollY + rect.top;
            if (paragraphTop - offset >= viewportTop) {
                current = i;
                return;
            }
        }
        current = paragraphs.length; // in case scrolled past all
    }

    function scrollToParagraph(index) {
        if (index >= 0 && index < paragraphs.length) {
            const rect = paragraphs[index].getBoundingClientRect();
            const scrollY = window.scrollY + rect.top - offset;

            window.scrollTo({
                top: Math.max(scrollY, 0),
                behavior: 'smooth'
            });

            current = index;
        }
    }

    document.addEventListener('keydown', (e) => {
        // Ignore keypresses in input fields or editable areas
        const target = e.target;
        if (
            target.tagName === 'INPUT' ||
            target.tagName === 'TEXTAREA' ||
            target.isContentEditable
        ) {
            return;
        }

        if (!initialized) init();

        if (e.key === 'ArrowRight') {
            e.preventDefault();
            if (current < paragraphs.length) {
                scrollToParagraph(current);
                current++;
            }
        } else if (e.key === 'ArrowLeft') {
            e.preventDefault();
            current = Math.max(current - 2, 0); // adjust for previous move
            scrollToParagraph(current);
            current++;
        }
    });
})();