"use client";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { AnimatePresence, motion } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
/**
* Desktop surface for the context/model pickers.
*
* The chips live wherever the screen puts them: at the bottom of the chat
* column, or inside a scrolling card (Teach it back's start). An absolutely
* positioned panel is then clipped by the scrolling ancestor — the teach model
* list opened upward and lost its first rows off the top of the viewport — so
* this renders the panel in a portal on
, `position: fixed`, anchored to
* the trigger that opened it. It opens on the side with room, is capped to the
* viewport (the panel scrolls, not the page), and closes on resize, on an
* outside press or when the page scrolls away from its chip — never floating
* loose. Phones keep their bottom sheet.
*/
/** Gap between the panel and the chip, and the minimum distance to the edge. */
const GAP = 8;
/** Below this there is not enough room to open on that side. */
const MIN_ROOM = 240;
export function PickerPopover({
open,
label,
align = "left",
width = "w-80",
onClose,
children,
}: {
open: boolean;
/** Accessible name of the panel (role="dialog"). */
label: string;
/** Which chip edge the panel lines up with. */
align?: "left" | "right";
/** Panel width utility class. */
width?: string;
onClose: () => void;
children: React.ReactNode;
}) {
const panel = useRef(null);
const reduceMotion = useReducedMotion();
const [style, setStyle] = useState(null);
/* Anchor to the open chip and fit inside the viewport. */
useLayoutEffect(() => {
if (!open) {
setStyle(null);
return;
}
const place = () => {
const node = panel.current;
if (!node) return;
const anchor = document.querySelector('[data-picker-trigger][aria-expanded="true"]');
const rect = anchor?.getBoundingClientRect();
const panelWidth = node.offsetWidth || 320;
/* The full content height, before any cap is applied. */
const natural = node.scrollHeight || node.offsetHeight || 320;
const below = rect ? window.innerHeight - rect.bottom - GAP * 2 : MIN_ROOM;
const above = rect ? rect.top - GAP * 2 : MIN_ROOM;
/* Prefer opening downwards when there is room; otherwise up, and split
the difference when neither side is comfortable. */
const placement = below >= MIN_ROOM || below >= above ? "bottom" : "top";
const room = Math.max(MIN_ROOM, placement === "bottom" ? below : above);
const height = Math.min(natural, room);
const rawLeft = rect ? (align === "right" ? rect.right - panelWidth : rect.left) : GAP;
const left = Math.max(GAP, Math.min(rawLeft, window.innerWidth - panelWidth - GAP));
/* Anchored to the chip on whichever side it opens: the panel's near edge
sits a GAP away from it, never drifting to the viewport edge. */
const top =
placement === "bottom"
? (rect?.bottom ?? GAP) + GAP
: Math.max(GAP, (rect?.top ?? height + GAP * 2) - GAP - height);
setStyle({ top, left, maxHeight: room });
};
place();
const close = () => onClose();
/* A scroll inside the panel (the model list) is not the page moving. */
const onScroll = (event: Event) => {
if (panel.current && event.target instanceof Node && panel.current.contains(event.target)) return;
onClose();
};
window.addEventListener("resize", close);
window.addEventListener("scroll", onScroll, true);
return () => {
window.removeEventListener("resize", close);
window.removeEventListener("scroll", onScroll, true);
};
}, [open, align, onClose]);
/* Desktop dropdown: a press anywhere outside closes it, except on the trigger
chips, which toggle their own popover. */
useEffect(() => {
if (!open) return;
const onDown = (event: PointerEvent) => {
const target = event.target;
if (!(target instanceof Element)) return;
if (panel.current?.contains(target)) return;
if (target.closest("[data-picker-trigger]")) return;
onClose();
};
document.addEventListener("pointerdown", onDown);
return () => document.removeEventListener("pointerdown", onDown);
}, [open, onClose]);
if (typeof document === "undefined") return null;
return createPortal(
{open ? (
{children}
) : null}
,
document.body,
);
}