"use client"; import { useAui, useAuiState } from "@assistant-ui/store"; import { useResource } from "@assistant-ui/tap/react"; import type { Unstable_TriggerAdapter } from "@assistant-ui/core"; import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, type ComponentPropsWithoutRef, type ComponentRef, } from "react"; import { Primitive } from "../../../utils/Primitive"; import { useComposerInputPluginRegistryOptional } from "../ComposerInputPluginContext"; import { TriggerPopoverResource, type TriggerPopoverResourceOutput, } from "./TriggerPopoverResource"; import type { TriggerBehavior } from "./triggerSelectionResource"; import { useTriggerPopoverAriaPublish, useTriggerPopoverRootContext, } from "./TriggerPopoverRootContext"; const TriggerPopoverScopeContext = createContext(null); export const useTriggerPopoverScopeContext = () => { const ctx = useContext(TriggerPopoverScopeContext); if (!ctx) throw new Error( "useTriggerPopoverScopeContext must be used within ComposerPrimitive.TriggerPopover", ); return ctx; }; export const useTriggerPopoverScopeContextOptional = () => useContext(TriggerPopoverScopeContext); /** Registration API exposed to behavior sub-primitives. */ export type TriggerBehaviorRegistration = { register(behavior: TriggerBehavior): () => void; }; const TriggerBehaviorRegistrationContext = createContext(null); /** Obtain the registration handle from the parent ``. */ export const useTriggerBehaviorRegistration = () => { const ctx = useContext(TriggerBehaviorRegistrationContext); if (!ctx) throw new Error( "TriggerPopover.Directive / TriggerPopover.Action must be rendered inside ComposerPrimitive.TriggerPopover", ); return ctx; }; export namespace ComposerPrimitiveTriggerPopover { export type Element = ComponentRef; export type Props = Omit< ComponentPropsWithoutRef, "onSelect" > & { /** The character(s) that activate this trigger (e.g. `"@"`, `"/"`). Also serves as the trigger identity within the root. */ readonly char: string; /** Adapter providing categories and items. */ readonly adapter?: Unstable_TriggerAdapter | undefined; }; } /** * Declares a trigger and renders its popover container. The popover only * renders its DOM (and children) when the trigger character is active in the * composer input and a behavior sub-primitive has been registered. * * A behavior is contributed by rendering exactly one of * `` or `` as a child. Without * a behavior the trigger stays closed. * * Must be placed inside `ComposerPrimitive.Unstable_TriggerPopoverRoot`. * * @example * ```tsx * * * * {(cats) => cats.map(...)} * * * {(items) => items.map(...)} * * * ``` */ export const ComposerPrimitiveTriggerPopover = forwardRef< ComposerPrimitiveTriggerPopover.Element, ComposerPrimitiveTriggerPopover.Props >( ( { char, adapter, "aria-label": ariaLabel, children, ...props }, forwardedRef, ) => { const aui = useAui(); const text = useAuiState((s) => s.composer.text); const popoverId = useId(); // Track in state (for resource reactivity) + ref (dev warning on duplicate registrations). const behaviorRef = useRef(null); const [behavior, setBehavior] = useState(null); const registrationCountRef = useRef(0); const register = useCallback( (next) => { registrationCountRef.current += 1; if ( process.env.NODE_ENV !== "production" && registrationCountRef.current > 1 ) { console.warn( `[assistant-ui] TriggerPopover "${char}" received more than one behavior child. Exactly one or is allowed per TriggerPopover; the last registration wins.`, ); } behaviorRef.current = next; setBehavior(next); return () => { registrationCountRef.current = Math.max( 0, registrationCountRef.current - 1, ); if (behaviorRef.current === next) { behaviorRef.current = null; setBehavior(null); } }; }, [char], ); const registration = useMemo( () => ({ register }), [register], ); const resource = useResource( TriggerPopoverResource({ adapter, text, triggerChar: char, behavior: behavior ?? undefined, aui, popoverId, }), ); // Wrapper changes per render, but tap-stable methods inside don't. const resourceRef = useRef(resource); resourceRef.current = resource; const root = useTriggerPopoverRootContext(); useEffect(() => { return root.register({ char, ...(behavior ? { behavior } : {}), resource: resourceRef.current, }); }, [root, char, behavior]); const pluginRegistry = useComposerInputPluginRegistryOptional(); useEffect(() => { if (!pluginRegistry) return undefined; return pluginRegistry.register(resourceRef.current); }, [pluginRegistry]); const open = behavior !== null && resource.open; const aria = useTriggerPopoverAriaPublish(); useEffect(() => { if (!open) return undefined; return () => { aria.setActiveAria(char, null); }; }, [aria, char, open]); useEffect(() => { if (!open) return; aria.setActiveAria(char, { popoverId, highlightedItemId: resource.highlightedItemId, }); }, [aria, char, popoverId, open, resource.highlightedItemId]); return ( {open ? ( {children} ) : ( children )} ); }, ); ComposerPrimitiveTriggerPopover.displayName = "ComposerPrimitive.TriggerPopover";