Small, dependency-free hooks shared across the docs shell and components.
Fires a callback on Cmd/Ctrl + a key. Powers the ⌘K command search shortcut in the header.
"use client";
import { useEffect } from "react";
/**
* Fires `callback` when `key` is pressed together with Cmd (macOS) or Ctrl
* (Windows/Linux). Used for the ⌘K / Ctrl+K command search shortcut.
*/
export function useKeyboardShortcut(key: string, callback: () => void) {
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
const isModifierPressed = event.metaKey || event.ctrlKey;
if (isModifierPressed && event.key.toLowerCase() === key.toLowerCase()) {
event.preventDefault();
callback();
}
}
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [key, callback]);
}
Watches a list of heading ids with an IntersectionObserver and returns the one currently in view. Powers the "On This Page" sidebar.
"use client";
import { useEffect, useState } from "react";
/**
* Tracks which of the given heading ids is currently active in the viewport,
* so the "On This Page" table of contents can highlight the section the
* reader is looking at.
*
* Uses direct scroll-position checks instead of IntersectionObserver:
* IntersectionObserver only reports elements whose intersection state
* *changed* in a given callback batch, not the full observed set — with
* several tall sections overlapping the viewport at once, that made the
* "active" pick effectively arbitrary. This instead walks `headingIds` in
* document order and picks the last one that has scrolled past a detection
* line at the vertical center of the viewport.
*
* The line sits at the center (not near the top) specifically because
* clicking a TOC link scrolls its heading to `block: "center"` — using the
* same line here means the item you just clicked is immediately detected
* as active, instead of the (possibly short) section above it.
*/
export function useActiveHeading(headingIds: string[]) {
const [activeId, setActiveId] = useState<string | null>(null);
useEffect(() => {
if (headingIds.length === 0) return;
let ticking = false;
function updateActiveId() {
ticking = false;
const detectionLine = window.innerHeight / 2;
let current: string | null = null;
for (const id of headingIds) {
const element = document.getElementById(id);
if (!element) continue;
if (element.getBoundingClientRect().top - detectionLine <= 0) {
current = id;
} else {
break;
}
}
setActiveId(current ?? headingIds[0]);
}
function onScroll() {
if (ticking) return;
ticking = true;
requestAnimationFrame(updateActiveId);
}
updateActiveId();
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onScroll);
};
}, [headingIds]);
return activeId;
}