);
}
export function PreviewRail({
items,
label = "Section navigation",
orientation = "vertical",
activeId,
defaultActiveId,
onActiveChange,
onItemSelect,
renderPreview,
showPreview = true,
previewSide = "after",
highlightActive = false,
itemSize = 24,
children,
className,
railClassName,
previewContainerClassName,
previewClassName,
}: PreviewRailProps) {
const uid = useId();
const reduce = useReducedMotion();
const rootRef = useRef(null);
const [internalActiveId, setInternalActiveId] = useState(
defaultActiveId ?? items[0]?.id ?? "",
);
const [hoveredId, setHoveredId] = useState(null);
// A finger cannot hover, so a tap lights the tick instead. Kept apart from
// the hovered one: they end in different ways, and a stray mouse move must
// not clear a tick the keyboard or a tap chose.
const [pinnedId, setPinnedId] = useState(null);
const [focusedId, setFocusedId] = useState(null);
// A click carries no pointerType, so the pointerdown before it is what says
// whether the activation was a tap. Keyboard activation has none at all.
const tap = useTapGesture();
const hover = useHoverGesture();
const clearPinned = useCallback(() => setPinnedId(null), []);
// The next tap outside the rail stands in for the pointer leaving it. The
// card is a preview, so that tap passes through to whatever it landed on.
useDismiss(pinnedId !== null, clearPinned, rootRef);
const requestedActiveId = activeId ?? internalActiveId;
const selectedId = items.some((item) => item.id === requestedActiveId)
? requestedActiveId
: (items[0]?.id ?? "");
const displayedId = hoveredId ?? pinnedId ?? focusedId ?? "";
const highlightedId = displayedId || (highlightActive ? selectedId : "");
const displayedIndex = items.findIndex((item) => item.id === highlightedId);
const rowTemplate = items.length
? `repeat(${items.length}, ${itemSize}px)`
: undefined;
const isHorizontal = orientation === "horizontal";
const selectItem = (id: string) => {
if (activeId === undefined) setInternalActiveId(id);
onActiveChange?.(id);
};
return (
{
// Both tick sources leave with the focus: a tap does not always land
// focus, but when it does, tabbing away must not strand the card.
if (!event.currentTarget.contains(event.relatedTarget)) {
setFocusedId(null);
setPinnedId(null);
}
}}
className={cn(
"isolate relative flex w-full overflow-visible",
isHorizontal
? "min-h-64 flex-col items-center justify-center"
: "min-h-80",
className,
)}
>
{showPreview ? (