···11+export { default as Root } from './popover/popover-root';
22+export { default as Surface } from './popover/popover-surface';
33+export { default as Trigger } from './popover/popover-trigger';
44+55+export type { PopoverRootProps } from './popover/popover-root';
66+export type { PopoverSurfaceProps } from './popover/popover-surface';
77+export type { PopoverTriggerChildProps, PopoverTriggerProps } from './popover/popover-trigger';
88+export type { PopoverContextValue } from './popover/context';
+45
src/primitives/popover/context.tsx
···11+import type { Placement } from '@floating-ui/dom';
22+import { createContext, useContext, type Accessor } from 'solid-js';
33+44+// #region types
55+66+export interface PopoverContextValue {
77+ /** whether the popover is open */
88+ open: Accessor<boolean>;
99+ /** set the popover open state */
1010+ setOpen: (open: boolean, eventType?: string) => void;
1111+ /** the trigger element ref */
1212+ triggerRef: Accessor<HTMLElement | null>;
1313+ /** set the trigger element ref */
1414+ setTriggerRef: (el: HTMLElement | null) => void;
1515+ /** the surface element ref */
1616+ surfaceRef: Accessor<HTMLElement | null>;
1717+ /** set the surface element ref */
1818+ setSurfaceRef: (el: HTMLElement | null) => void;
1919+ /** unique ID for the trigger */
2020+ triggerId: string;
2121+ /** unique ID for the surface */
2222+ surfaceId: string;
2323+ /** placement for the surface */
2424+ placement: Accessor<Placement>;
2525+ /** whether to open on hover */
2626+ openOnHover: Accessor<boolean>;
2727+}
2828+2929+// #endregion
3030+3131+// #region context
3232+3333+const PopoverContext = createContext<PopoverContextValue>();
3434+3535+export const PopoverProvider = PopoverContext.Provider;
3636+3737+export function usePopoverContext(): PopoverContextValue {
3838+ const ctx = useContext(PopoverContext);
3939+ if (!ctx) {
4040+ throw new Error('Popover components must be used within a Popover.Root');
4141+ }
4242+ return ctx;
4343+}
4444+4545+// #endregion
+107
src/primitives/popover/popover-root.tsx
···11+import type { Placement } from '@floating-ui/dom';
22+import { createMemo, createSignal, createUniqueId, onCleanup, type JSX } from 'solid-js';
33+44+import { PopoverProvider, type PopoverContextValue } from './context';
55+66+// #region types
77+88+export interface PopoverRootProps {
99+ /** popover content (PopoverTrigger and PopoverSurface) */
1010+ children: JSX.Element;
1111+ /** controlled open state */
1212+ open?: boolean;
1313+ /** default open state for uncontrolled usage */
1414+ defaultOpen?: boolean;
1515+ /** callback when open state changes */
1616+ onOpenChange?: (open: boolean) => void;
1717+ /** whether to open on hover instead of click */
1818+ openOnHover?: boolean;
1919+ /** delay in ms before opening on hover */
2020+ mouseEnterDelay?: number;
2121+ /** delay in ms before closing after mouse leaves */
2222+ mouseLeaveDelay?: number;
2323+ /** placement of the surface relative to trigger */
2424+ placement?: Placement;
2525+}
2626+2727+// #endregion
2828+2929+// #region component
3030+3131+const PopoverRoot = (props: PopoverRootProps) => {
3232+ const triggerId = createUniqueId();
3333+ const surfaceId = createUniqueId();
3434+3535+ const [triggerRef, setTriggerRef] = createSignal<HTMLElement | null>(null);
3636+ const [surfaceRef, setSurfaceRef] = createSignal<HTMLElement | null>(null);
3737+3838+ // support both controlled and uncontrolled modes
3939+ const [internalOpen, setInternalOpen] = createSignal(props.defaultOpen ?? false);
4040+4141+ const open = () => props.open ?? internalOpen();
4242+4343+ const mouseEnterDelay = () => props.mouseEnterDelay ?? 0;
4444+ const mouseLeaveDelay = () => props.mouseLeaveDelay ?? 500;
4545+4646+ let openTimeout: ReturnType<typeof setTimeout> | undefined;
4747+ let closeTimeout: ReturnType<typeof setTimeout> | undefined;
4848+4949+ const clearTimeouts = () => {
5050+ if (openTimeout) {
5151+ clearTimeout(openTimeout);
5252+ openTimeout = undefined;
5353+ }
5454+ if (closeTimeout) {
5555+ clearTimeout(closeTimeout);
5656+ closeTimeout = undefined;
5757+ }
5858+ };
5959+6060+ onCleanup(clearTimeouts);
6161+6262+ const setOpen = (value: boolean, eventType?: string) => {
6363+ clearTimeouts();
6464+6565+ const updateState = (newValue: boolean) => {
6666+ if (props.open === undefined) {
6767+ setInternalOpen(newValue);
6868+ }
6969+ props.onOpenChange?.(newValue);
7070+ };
7171+7272+ if (eventType === 'pointerenter') {
7373+ // opening on hover - apply enter delay
7474+ const delay = mouseEnterDelay();
7575+ if (delay > 0) {
7676+ openTimeout = setTimeout(() => updateState(true), delay);
7777+ } else {
7878+ updateState(true);
7979+ }
8080+ } else if (eventType === 'pointerleave') {
8181+ // closing on hover - apply leave delay
8282+ closeTimeout = setTimeout(() => updateState(false), mouseLeaveDelay());
8383+ } else {
8484+ // click or other events - immediate
8585+ updateState(value);
8686+ }
8787+ };
8888+8989+ const context: PopoverContextValue = {
9090+ open,
9191+ setOpen,
9292+ triggerRef,
9393+ setTriggerRef,
9494+ surfaceRef,
9595+ setSurfaceRef,
9696+ triggerId,
9797+ surfaceId,
9898+ placement: createMemo(() => props.placement ?? 'bottom-start'),
9999+ openOnHover: createMemo(() => props.openOnHover ?? false),
100100+ };
101101+102102+ return <PopoverProvider value={context}>{props.children}</PopoverProvider>;
103103+};
104104+105105+export default PopoverRoot;
106106+107107+// #endregion