"use client"; import type { ComponentProps, HTMLAttributes, ReactElement, ReactNode } from "react"; import { useCallback, useEffect, useRef, useSyncExternalStore } from "react"; import { Popover, PopoverContent, PopoverTrigger } from "./_adapter"; type PopoverContentProps = ComponentProps; export type CitationHoverTriggerProps = Pick< HTMLAttributes, "onBlur" | "onFocus" | "onPointerEnter" | "onPointerLeave" >; interface CitationHoverPopoverProps { id: string; trigger: (props: CitationHoverTriggerProps) => ReactElement; children: ReactNode; contentClassName?: string; side?: PopoverContentProps["side"]; align?: PopoverContentProps["align"]; sideOffset?: PopoverContentProps["sideOffset"]; onContentClick?: PopoverContentProps["onClick"]; } const OPEN_DELAY_MS = 80; const CLOSE_DELAY_MS = 120; let activeCitationId: string | null = null; const listeners = new Set<() => void>(); function subscribe(listener: () => void) { listeners.add(listener); return () => { listeners.delete(listener); }; } function getSnapshot() { return activeCitationId; } function setActiveCitationId(id: string | null) { if (activeCitationId === id) return; activeCitationId = id; for (const listener of listeners) { listener(); } } function useCitationHoverState(id: string) { const activeId = useSyncExternalStore(subscribe, getSnapshot, getSnapshot); const openTimerRef = useRef | null>(null); const closeTimerRef = useRef | null>(null); const clearTimers = useCallback(() => { if (openTimerRef.current) { clearTimeout(openTimerRef.current); openTimerRef.current = null; } if (closeTimerRef.current) { clearTimeout(closeTimerRef.current); closeTimerRef.current = null; } }, []); const open = activeId === id; const scheduleOpen = useCallback(() => { clearTimers(); openTimerRef.current = setTimeout(() => { setActiveCitationId(id); openTimerRef.current = null; }, OPEN_DELAY_MS); }, [clearTimers, id]); const scheduleClose = useCallback(() => { clearTimers(); closeTimerRef.current = setTimeout(() => { if (activeCitationId === id) { setActiveCitationId(null); } closeTimerRef.current = null; }, CLOSE_DELAY_MS); }, [clearTimers, id]); const handleOpenChange = useCallback( (nextOpen: boolean) => { clearTimers(); setActiveCitationId(nextOpen ? id : null); }, [clearTimers, id] ); useEffect(() => { return () => { clearTimers(); if (activeCitationId === id) { setActiveCitationId(null); } }; }, [clearTimers, id]); return { open, scheduleOpen, scheduleClose, handleOpenChange }; } export function CitationHoverPopover({ id, trigger, children, contentClassName, side = "top", align = "start", sideOffset = 6, onContentClick, }: CitationHoverPopoverProps) { const { open, scheduleOpen, scheduleClose, handleOpenChange } = useCitationHoverState(id); const hoverProps = { onPointerEnter: scheduleOpen, onPointerLeave: scheduleClose, onFocus: scheduleOpen, onBlur: scheduleClose, } satisfies CitationHoverTriggerProps; return ( {trigger(hoverProps)} event.preventDefault()} onCloseAutoFocus={(event) => event.preventDefault()} onClick={onContentClick} > {children} ); }