;
+ };
+}
diff --git a/packages/ui/uikit/headless/components/src/components/Dialog/description/DialogDescription.tsx b/packages/ui/uikit/headless/components/src/components/Dialog/description/DialogDescription.tsx
new file mode 100644
index 00000000..5c55f0ca
--- /dev/null
+++ b/packages/ui/uikit/headless/components/src/components/Dialog/description/DialogDescription.tsx
@@ -0,0 +1,50 @@
+'use client';
+
+import { useIsoLayoutEffect } from '@flippo_ui/hooks';
+
+import { useHeadlessUiId, useRenderElement } from '@lib/hooks';
+
+import type { HeadlessUIComponentProps } from '@lib/types';
+
+import { useDialogRootContext } from '../root/DialogRootContext';
+
+/**
+ * A paragraph with additional information about the dialog.
+ * Renders a `` element.
+ *
+ * Documentation: [Base UI Dialog](https://base-ui.com/react/components/dialog)
+ */
+export function DialogDescription(componentProps: DialogDescription.Props) {
+ const {
+ /* eslint-disable unused-imports/no-unused-vars */
+ className,
+ render,
+ /* eslint-enable unused-imports/no-unused-vars */
+ id: idProp,
+ ref,
+ ...elementProps
+ } = componentProps;
+ const { setDescriptionElementId } = useDialogRootContext();
+
+ const id = useHeadlessUiId(idProp);
+
+ useIsoLayoutEffect(() => {
+ setDescriptionElementId(id);
+ return () => {
+ setDescriptionElementId(undefined);
+ };
+ }, [id, setDescriptionElementId]);
+
+ const element = useRenderElement('p', componentProps, {
+ ref,
+ props: [{ id }, elementProps]
+ });
+
+ return element;
+}
+
+export namespace DialogDescription {
+ export type State = object;
+
+ export type Props = HeadlessUIComponentProps<'p', State>;
+}
diff --git a/packages/ui/uikit/headless/components/src/components/Dialog/index.parts.ts b/packages/ui/uikit/headless/components/src/components/Dialog/index.parts.ts
new file mode 100644
index 00000000..c6b2a98d
--- /dev/null
+++ b/packages/ui/uikit/headless/components/src/components/Dialog/index.parts.ts
@@ -0,0 +1,8 @@
+export { DialogBackdrop as Backdrop } from './backdrop/DialogBackdrop';
+export { DialogClose as Close } from './close/DialogClose';
+export { DialogDescription as Description } from './description/DialogDescription';
+export { DialogPopup as Popup } from './popup/DialogPopup';
+export { DialogPortal as Portal } from './portal/DialogPortal';
+export { DialogRoot as Root } from './root/DialogRoot';
+export { DialogTitle as Title } from './title/DialogTitle';
+export { DialogTrigger as Trigger } from './trigger/DialogTrigger';
diff --git a/packages/ui/uikit/headless/components/src/components/Dialog/index.ts b/packages/ui/uikit/headless/components/src/components/Dialog/index.ts
new file mode 100644
index 00000000..823c17d2
--- /dev/null
+++ b/packages/ui/uikit/headless/components/src/components/Dialog/index.ts
@@ -0,0 +1 @@
+export * as Dialog from './index.parts';
diff --git a/packages/ui/uikit/headless/components/src/components/Dialog/popup/DialogPopup.tsx b/packages/ui/uikit/headless/components/src/components/Dialog/popup/DialogPopup.tsx
new file mode 100644
index 00000000..8081b070
--- /dev/null
+++ b/packages/ui/uikit/headless/components/src/components/Dialog/popup/DialogPopup.tsx
@@ -0,0 +1,179 @@
+'use client';
+
+import React from 'react';
+
+import { useMergedRef, useOpenChangeComplete } from '@flippo_ui/hooks';
+
+import type { TInteraction, TransitionStatus } from '@flippo_ui/hooks';
+
+import { useRenderElement } from '@lib/hooks';
+import { InternalBackdrop } from '@lib/InternalBackdrop';
+import { popupStateMapping } from '@lib/popupStateMapping';
+import { transitionStatusMapping } from '@lib/styleHookMapping';
+import { FloatingFocusManager } from '@packages/floating-ui-react';
+
+import type { CustomStyleHookMapping } from '@lib/getStyleHookProps';
+import type { HeadlessUIComponentProps } from '@lib/types';
+
+import { useDialogPortalContext } from '../portal/DialogPortalContext';
+import { useDialogRootContext } from '../root/DialogRootContext';
+
+import { DialogPopupCssVars } from './DialogPopupCssVars';
+import { DialogPopupDataAttributes } from './DialogPopupDataAttributes';
+import { useDialogPopup } from './useDialogPopup';
+
+const customStyleHookMapping: CustomStyleHookMapping = {
+ ...popupStateMapping,
+ ...transitionStatusMapping,
+ nestedDialogOpen(value) {
+ return value ? { [DialogPopupDataAttributes.nestedDialogOpen]: '' } : null;
+ }
+};
+
+/**
+ * A container for the dialog contents.
+ * Renders a `` element.
+ *
+ * Documentation: [Base UI Dialog](https://base-ui.com/react/components/dialog)
+ */
+export function DialogPopup(componentProps: DialogPopup.Props) {
+ const {
+ /* eslint-disable unused-imports/no-unused-vars */
+ className,
+ render,
+ /* eslint-enable unused-imports/no-unused-vars */
+ finalFocus,
+ initialFocus,
+ ref,
+ ...elementProps
+ } = componentProps;
+
+ const {
+ descriptionElementId,
+ dismissible,
+ floatingRootContext,
+ getPopupProps,
+ modal,
+ mounted,
+ nested,
+ nestedOpenDialogCount,
+ setOpen,
+ open,
+ openMethod,
+ popupRef,
+ setPopupElement,
+ titleElementId,
+ transitionStatus,
+ onOpenChangeComplete,
+ internalBackdropRef
+ } = useDialogRootContext();
+
+ useDialogPortalContext();
+
+ useOpenChangeComplete({
+ open,
+ ref: popupRef,
+ onComplete() {
+ if (open) {
+ onOpenChangeComplete?.(true);
+ }
+ }
+ });
+
+ const mergedRef = useMergedRef(ref, popupRef);
+
+ const { popupProps, resolvedInitialFocus } = useDialogPopup({
+ descriptionElementId,
+ initialFocus,
+ mounted,
+ setOpen,
+ openMethod,
+ ref: mergedRef,
+ setPopupElement,
+ titleElementId
+ });
+
+ const nestedDialogOpen = nestedOpenDialogCount > 0;
+
+ const state: DialogPopup.State = React.useMemo(
+ () => ({
+ open,
+ nested,
+ transitionStatus,
+ nestedDialogOpen
+ }),
+ [
+ open,
+ nested,
+ transitionStatus,
+ nestedDialogOpen
+ ]
+ );
+
+ const element = useRenderElement('div', componentProps, {
+ state,
+ props: [
+ getPopupProps(),
+ popupProps,
+ {
+ style: {
+ [DialogPopupCssVars.nestedDialogs]: nestedOpenDialogCount
+ } as React.CSSProperties
+ },
+ elementProps
+ ],
+ customStyleHookMapping
+ });
+
+ return (
+
+ {mounted && modal === true && (
+
+ )}
+
+ {element}
+
+
+ );
+}
+
+export namespace DialogPopup {
+ export type State = {
+ /**
+ * Whether the dialog is currently open.
+ */
+ open: boolean;
+ transitionStatus: TransitionStatus;
+ /**
+ * Whether the dialog is nested within a parent dialog.
+ */
+ nested: boolean;
+ /**
+ * Whether the dialog has nested dialogs open.
+ */
+ nestedDialogOpen: boolean;
+ };
+
+ export type Props = {
+ /**
+ * Determines the element to focus when the dialog is opened.
+ * By default, the first focusable element is focused.
+ */
+ initialFocus?:
+ | React.RefObject
+ | ((interactionType: TInteraction) => React.RefObject);
+ /**
+ * Determines the element to focus when the dialog is closed.
+ * By default, focus returns to the trigger.
+ */
+ finalFocus?: React.RefObject;
+ } & HeadlessUIComponentProps<'div', State>;
+}
diff --git a/packages/ui/uikit/headless/components/src/components/Dialog/popup/DialogPopupCssVars.ts b/packages/ui/uikit/headless/components/src/components/Dialog/popup/DialogPopupCssVars.ts
new file mode 100644
index 00000000..0be15365
--- /dev/null
+++ b/packages/ui/uikit/headless/components/src/components/Dialog/popup/DialogPopupCssVars.ts
@@ -0,0 +1,7 @@
+export enum DialogPopupCssVars {
+ /**
+ * Indicates how many dialogs are nested within.
+ * @type {number}
+ */
+ nestedDialogs = '--nested-dialogs'
+}
diff --git a/packages/ui/uikit/headless/components/src/components/Dialog/popup/DialogPopupDataAttributes.ts b/packages/ui/uikit/headless/components/src/components/Dialog/popup/DialogPopupDataAttributes.ts
new file mode 100644
index 00000000..66099081
--- /dev/null
+++ b/packages/ui/uikit/headless/components/src/components/Dialog/popup/DialogPopupDataAttributes.ts
@@ -0,0 +1,28 @@
+import { CommonPopupDataAttributes } from '@lib/popupStateMapping';
+
+export enum DialogPopupDataAttributes {
+ /**
+ * Present when the dialog is open.
+ */
+ open = CommonPopupDataAttributes.open,
+ /**
+ * Present when the dialog is closed.
+ */
+ closed = CommonPopupDataAttributes.closed,
+ /**
+ * Present when the dialog is animating in.
+ */
+ startingStyle = CommonPopupDataAttributes.startingStyle,
+ /**
+ * Present when the dialog is animating out.
+ */
+ endingStyle = CommonPopupDataAttributes.endingStyle,
+ /**
+ * Present when the dialog is nested within another dialog.
+ */
+ nested = 'data-nested',
+ /**
+ * Present when the dialog has other open dialogs nested within it.
+ */
+ nestedDialogOpen = 'data-nested-dialog-open'
+}
diff --git a/packages/ui/uikit/headless/components/src/components/Dialog/popup/useDialogPopup.ts b/packages/ui/uikit/headless/components/src/components/Dialog/popup/useDialogPopup.ts
new file mode 100644
index 00000000..6443f66e
--- /dev/null
+++ b/packages/ui/uikit/headless/components/src/components/Dialog/popup/useDialogPopup.ts
@@ -0,0 +1,117 @@
+'use client';
+
+import React from 'react';
+
+import { useMergedRef } from '@flippo_ui/hooks';
+
+import type { TInteraction } from '@flippo_ui/hooks';
+
+import type { HTMLProps } from '@lib/types';
+
+import { COMPOSITE_KEYS } from '../../Composite/composite';
+
+import type { DialogRoot } from '../root/DialogRoot';
+
+export function useDialogPopup(parameters: useDialogPopup.Parameters): useDialogPopup.ReturnValue {
+ const {
+ descriptionElementId,
+ initialFocus,
+ mounted,
+ openMethod,
+ ref,
+ setPopupElement,
+ titleElementId
+ } = parameters;
+
+ const popupRef = React.useRef(null);
+
+ const handleRef = useMergedRef(ref, popupRef, setPopupElement);
+
+ // Default initial focus logic:
+ // If opened by touch, focus the popup element to prevent the virtual keyboard from opening
+ // (this is required for Android specifically as iOS handles this automatically).
+ const defaultInitialFocus = React.useCallback((interactionType: TInteraction) => {
+ if (interactionType === 'touch') {
+ return popupRef;
+ }
+
+ return 0;
+ }, []);
+
+ const resolvedInitialFocus = React.useMemo(() => {
+ if (initialFocus == null) {
+ return defaultInitialFocus(openMethod ?? '');
+ }
+
+ if (typeof initialFocus === 'function') {
+ return initialFocus(openMethod ?? '');
+ }
+
+ return initialFocus;
+ }, [defaultInitialFocus, initialFocus, openMethod]);
+
+ const popupProps: HTMLProps = {
+ 'aria-labelledby': titleElementId ?? undefined,
+ 'aria-describedby': descriptionElementId ?? undefined,
+ 'role': 'dialog',
+ 'tabIndex': -1,
+ 'ref': handleRef,
+ 'hidden': !mounted,
+ onKeyDown(event) {
+ if (COMPOSITE_KEYS.has(event.key)) {
+ event.stopPropagation();
+ }
+ }
+ };
+
+ return {
+ popupProps,
+ resolvedInitialFocus
+ };
+}
+
+export namespace useDialogPopup {
+ export type Parameters = {
+ /**
+ * The ref to the dialog element.
+ */
+ ref: React.Ref;
+ openMethod: TInteraction | null;
+ /**
+ * Event handler called when the dialog is opened or closed.
+ */
+ setOpen: (
+ open: boolean,
+ event: Event | undefined,
+ reason: DialogRoot.OpenChangeReason | undefined,
+ ) => void;
+ /**
+ * The id of the title element associated with the dialog.
+ */
+ titleElementId: string | undefined;
+ /**
+ * The id of the description element associated with the dialog.
+ */
+ descriptionElementId: string | undefined;
+ /**
+ * Determines the element to focus when the dialog is opened.
+ * By default, the first focusable element is focused.
+ */
+ initialFocus?:
+ | React.RefObject
+ | ((interactionType: TInteraction) => React.RefObject);
+ /**
+ * Determines if the dialog should be mounted.
+ */
+ mounted: boolean;
+ /**
+ * Callback to register the popup element.
+ */
+ setPopupElement: React.Dispatch>;
+ };
+
+ export type ReturnValue = {
+ popupProps: HTMLProps;
+ resolvedInitialFocus: React.RefObject | number;
+ };
+}
diff --git a/packages/ui/uikit/headless/components/src/components/Dialog/portal/DialogPortal.tsx b/packages/ui/uikit/headless/components/src/components/Dialog/portal/DialogPortal.tsx
new file mode 100644
index 00000000..edbc7275
--- /dev/null
+++ b/packages/ui/uikit/headless/components/src/components/Dialog/portal/DialogPortal.tsx
@@ -0,0 +1,49 @@
+'use client';
+
+import React from 'react';
+
+import { FloatingPortal } from '@packages/floating-ui-react';
+
+import type { FloatingPortalProps } from '@packages/floating-ui-react';
+
+import { useDialogRootContext } from '../root/DialogRootContext';
+
+import { DialogPortalContext } from './DialogPortalContext';
+
+/**
+ * A portal element that moves the popup to a different part of the DOM.
+ * By default, the portal element is appended to ``.
+ *
+ * Documentation: [Base UI Dialog](https://base-ui.com/react/components/dialog)
+ */
+export function DialogPortal(props: DialogPortal.Props) {
+ const { children, keepMounted = false, container } = props;
+
+ const { mounted } = useDialogRootContext();
+
+ const shouldRender = mounted || keepMounted;
+ if (!shouldRender) {
+ return null;
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+export namespace DialogPortal {
+ export type Props = {
+ children?: React.ReactNode;
+ /**
+ * Whether to keep the portal mounted in the DOM while the popup is hidden.
+ * @default false
+ */
+ keepMounted?: boolean;
+ /**
+ * A parent element to render the portal element into.
+ */
+ container?: FloatingPortalProps['root'];
+ };
+}
diff --git a/packages/ui/uikit/headless/components/src/components/Dialog/portal/DialogPortalContext.ts b/packages/ui/uikit/headless/components/src/components/Dialog/portal/DialogPortalContext.ts
new file mode 100644
index 00000000..06943aa1
--- /dev/null
+++ b/packages/ui/uikit/headless/components/src/components/Dialog/portal/DialogPortalContext.ts
@@ -0,0 +1,14 @@
+'use client';
+
+import React from 'react';
+
+export const DialogPortalContext = React.createContext(undefined);
+
+export function useDialogPortalContext() {
+ const value = React.use(DialogPortalContext);
+
+ if (value === undefined) {
+ throw new Error('Headless UI: is missing.');
+ }
+ return value;
+}
diff --git a/packages/ui/uikit/headless/components/src/components/Dialog/root/DialogRoot.tsx b/packages/ui/uikit/headless/components/src/components/Dialog/root/DialogRoot.tsx
new file mode 100644
index 00000000..bce75ce2
--- /dev/null
+++ b/packages/ui/uikit/headless/components/src/components/Dialog/root/DialogRoot.tsx
@@ -0,0 +1,75 @@
+'use client';
+
+import React from 'react';
+
+import type { TBaseOpenChangeReason } from '@lib/translateOpenChangeReason';
+
+import { DialogContext } from '../utils/DialogContext';
+
+import { DialogRootContext, useOptionalDialogRootContext } from './DialogRootContext';
+import { useDialogRoot } from './useDialogRoot';
+
+/**
+ * Groups all parts of the dialog.
+ * Doesn’t render its own HTML element.
+ *
+ * Documentation: [Base UI Dialog](https://base-ui.com/react/components/dialog)
+ */
+export function DialogRoot(componentProps: DialogRoot.Props) {
+ const {
+ children,
+ defaultOpen = false,
+ dismissible = true,
+ modal = true,
+ onOpenChange,
+ open,
+ actionsRef,
+ onOpenChangeComplete
+ } = componentProps;
+
+ const parentDialogRootContext = useOptionalDialogRootContext();
+
+ const dialogRoot = useDialogRoot({
+ open,
+ defaultOpen,
+ onOpenChange,
+ modal,
+ dismissible,
+ actionsRef,
+ onOpenChangeComplete,
+ onNestedDialogClose: parentDialogRootContext?.onNestedDialogClose,
+ onNestedDialogOpen: parentDialogRootContext?.onNestedDialogOpen
+ });
+
+ const nested = Boolean(parentDialogRootContext);
+
+ const dialogContextValue = React.useMemo(
+ () => ({
+ ...dialogRoot,
+ nested,
+ onOpenChangeComplete
+ }),
+ [dialogRoot, nested, onOpenChangeComplete]
+ );
+ const dialogRootContextValue = React.useMemo(() => ({ dismissible }), [dismissible]);
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+export namespace DialogRoot {
+ export type Props = {
+ children?: React.ReactNode;
+ } & useDialogRoot.SharedParameters;
+
+ export type Actions = {
+ unmount: () => void;
+ };
+
+ export type OpenChangeReason = TBaseOpenChangeReason | 'close-press';
+}
diff --git a/packages/ui/uikit/headless/components/src/components/Dialog/root/DialogRootContext.ts b/packages/ui/uikit/headless/components/src/components/Dialog/root/DialogRootContext.ts
new file mode 100644
index 00000000..d23900d2
--- /dev/null
+++ b/packages/ui/uikit/headless/components/src/components/Dialog/root/DialogRootContext.ts
@@ -0,0 +1,44 @@
+'use client';
+
+import React from 'react';
+
+import { DialogContext } from '../utils/DialogContext';
+
+export type TDialogRootContext = {
+ /**
+ * Determines whether the dialog should close on outside clicks.
+ */
+ dismissible: boolean;
+};
+
+export const DialogRootContext = React.createContext(undefined);
+
+export function useOptionalDialogRootContext() {
+ const dialogRootContext = React.use(DialogRootContext);
+ const dialogContext = React.use(DialogContext);
+
+ if (dialogContext === undefined && dialogRootContext === undefined) {
+ return undefined;
+ }
+
+ return {
+ ...dialogRootContext,
+ ...dialogContext
+ };
+}
+
+export function useDialogRootContext() {
+ const dialogRootContext = React.use(DialogRootContext);
+ const dialogContext = React.use(DialogContext);
+
+ if (dialogContext === undefined) {
+ throw new Error(
+ 'Headless UI: DialogRootContext is missing. Dialog parts must be placed within .'
+ );
+ }
+
+ return {
+ ...dialogRootContext,
+ ...dialogContext
+ };
+}
diff --git a/packages/ui/uikit/headless/components/src/components/Dialog/root/useDialogRoot.ts b/packages/ui/uikit/headless/components/src/components/Dialog/root/useDialogRoot.ts
new file mode 100644
index 00000000..9e5a3bb7
--- /dev/null
+++ b/packages/ui/uikit/headless/components/src/components/Dialog/root/useDialogRoot.ts
@@ -0,0 +1,387 @@
+'use client';
+
+import React from 'react';
+
+import {
+ useControlledState,
+ useEventCallback,
+ useOpenChangeComplete,
+ useOpenInteractionType,
+ useScrollLock,
+ useTransitionStatus
+} from '@flippo_ui/hooks';
+
+import type { TInteraction, TransitionStatus } from '@flippo_ui/hooks';
+
+import { translateOpenChangeReason } from '@lib/translateOpenChangeReason';
+import {
+ useClick,
+ useDismiss,
+ useFloatingRootContext,
+ useInteractions,
+ useRole
+} from '@packages/floating-ui-react';
+import { getTarget } from '@packages/floating-ui-react/utils';
+
+import type { HTMLProps, RequiredExcept } from '@lib/types';
+import type { FloatingRootContext, OpenChangeReason as FloatingUIOpenChangeReason, PressType } from '@packages/floating-ui-react';
+
+import type { DialogRoot } from './DialogRoot';
+
+export function useDialogRoot(params: useDialogRoot.Parameters): useDialogRoot.ReturnValue {
+ const {
+ defaultOpen,
+ dismissible,
+ modal,
+ onNestedDialogClose,
+ onNestedDialogOpen,
+ onOpenChange: onOpenChangeParameter,
+ open: openParam,
+ onOpenChangeComplete
+ } = params;
+
+ const [open, setOpenUnwrapped] = useControlledState({
+ prop: openParam,
+ defaultProp: defaultOpen,
+ caller: 'DialogRoot'
+ });
+
+ const popupRef = React.useRef(null);
+ const backdropRef = React.useRef(null);
+ const internalBackdropRef = React.useRef(null);
+
+ const [titleElementId, setTitleElementId] = React.useState(undefined);
+ const [descriptionElementId, setDescriptionElementId] = React.useState(
+ undefined
+ );
+ const [triggerElement, setTriggerElement] = React.useState(null);
+ const [popupElement, setPopupElement] = React.useState(null);
+
+ const { mounted, setMounted, transitionStatus } = useTransitionStatus(open);
+ const {
+ openMethod,
+ triggerProps,
+ reset: resetOpenInteractionType
+ } = useOpenInteractionType(open);
+
+ const setOpen = useEventCallback(
+ (
+ nextOpen: boolean,
+ event: Event | undefined,
+ reason: DialogRoot.OpenChangeReason | undefined
+ ) => {
+ onOpenChangeParameter?.(nextOpen, event, reason);
+ setOpenUnwrapped(nextOpen);
+ }
+ );
+
+ const handleUnmount = useEventCallback(() => {
+ setMounted(false);
+ onOpenChangeComplete?.(false);
+ resetOpenInteractionType();
+ });
+
+ useOpenChangeComplete({
+ enabled: !params.actionsRef,
+ open,
+ ref: popupRef,
+ onComplete() {
+ if (!open) {
+ handleUnmount();
+ }
+ }
+ });
+
+ React.useImperativeHandle(params.actionsRef, () => ({ unmount: handleUnmount }), [handleUnmount]);
+
+ const handleFloatingUIOpenChange = (
+ nextOpen: boolean,
+ event: Event | undefined,
+ reason: FloatingUIOpenChangeReason | undefined
+ ) => {
+ setOpen(nextOpen, event, translateOpenChangeReason(reason));
+ };
+
+ const context = useFloatingRootContext({
+ elements: { reference: triggerElement, floating: popupElement },
+ open,
+ onOpenChange: handleFloatingUIOpenChange
+ });
+ const [ownNestedOpenDialogs, setOwnNestedOpenDialogs] = React.useState(0);
+ const isTopmost = ownNestedOpenDialogs === 0;
+
+ const role = useRole(context);
+ const click = useClick(context);
+ const dismiss = useDismiss(context, {
+ outsidePressEvent() {
+ if (internalBackdropRef.current || backdropRef.current) {
+ return 'intentional' as PressType;
+ }
+ // Ensure `aria-hidden` on outside elements is removed immediately
+ // on outside press when trapping focus.
+ return {
+ mouse: (modal === 'trap-focus' ? 'sloppy' : 'intentional') as PressType,
+ touch: 'sloppy' as PressType
+ };
+ },
+ outsidePress(event) {
+ if (event.button !== 0) {
+ return false;
+ }
+ const target = getTarget(event) as Element | null;
+ if (isTopmost && dismissible) {
+ const eventTarget = target as Element | null;
+ // Only close if the click occurred on the dialog's owning backdrop.
+ // This supports multiple modal dialogs that aren't nested in the React tree:
+ // https://github.com/mui/base-ui/issues/1320
+ if (modal) {
+ return internalBackdropRef.current || backdropRef.current
+ ? internalBackdropRef.current === eventTarget || backdropRef.current === eventTarget
+ : true;
+ }
+ return true;
+ }
+ return false;
+ },
+ escapeKey: isTopmost
+ });
+
+ useScrollLock({
+ enabled: open && modal === true,
+ mounted,
+ open,
+ referenceElement: popupElement
+ });
+
+ const { getReferenceProps, getFloatingProps } = useInteractions([role, click, dismiss]);
+
+ React.useEffect(() => {
+ if (onNestedDialogOpen && open) {
+ onNestedDialogOpen(ownNestedOpenDialogs);
+ }
+
+ if (onNestedDialogClose && !open) {
+ onNestedDialogClose();
+ }
+
+ return () => {
+ if (onNestedDialogClose && open) {
+ onNestedDialogClose();
+ }
+ };
+ }, [
+ open,
+ onNestedDialogClose,
+ onNestedDialogOpen,
+ ownNestedOpenDialogs
+ ]);
+
+ const handleNestedDialogOpen = React.useCallback((ownChildrenCount: number) => {
+ setOwnNestedOpenDialogs(ownChildrenCount + 1);
+ }, []);
+
+ const handleNestedDialogClose = React.useCallback(() => {
+ setOwnNestedOpenDialogs(0);
+ }, []);
+
+ const dialogTriggerProps = React.useMemo(
+ () => getReferenceProps(triggerProps),
+ [getReferenceProps, triggerProps]
+ );
+
+ return React.useMemo(() => {
+ return {
+ modal,
+ setOpen,
+ open,
+ titleElementId,
+ setTitleElementId,
+ descriptionElementId,
+ setDescriptionElementId,
+ onNestedDialogOpen: handleNestedDialogOpen,
+ onNestedDialogClose: handleNestedDialogClose,
+ nestedOpenDialogCount: ownNestedOpenDialogs,
+ openMethod,
+ mounted,
+ transitionStatus,
+ triggerProps: dialogTriggerProps,
+ getPopupProps: getFloatingProps,
+ setTriggerElement,
+ setPopupElement,
+ popupRef,
+ backdropRef,
+ internalBackdropRef,
+ floatingRootContext: context
+ } satisfies useDialogRoot.ReturnValue;
+ }, [
+ modal,
+ setOpen,
+ open,
+ titleElementId,
+ descriptionElementId,
+ handleNestedDialogOpen,
+ handleNestedDialogClose,
+ ownNestedOpenDialogs,
+ openMethod,
+ mounted,
+ transitionStatus,
+ dialogTriggerProps,
+ getFloatingProps,
+ context
+ ]);
+}
+
+export namespace useDialogRoot {
+ export type SharedParameters = {
+ /**
+ * Whether the dialog is currently open.
+ */
+ open?: boolean;
+ /**
+ * Whether the dialog is initially open.
+ *
+ * To render a controlled dialog, use the `open` prop instead.
+ * @default false
+ */
+ defaultOpen?: boolean;
+ /**
+ * Determines if the dialog enters a modal state when open.
+ * - `true`: user interaction is limited to just the dialog: focus is trapped,
+ * document page scroll is locked, and pointer interactions on outside elements are disabled.
+ * - `false`: user interaction with the rest of the document is allowed.
+ * - `'trap-focus'`: focus is trapped inside the dialog, but document page scroll is
+ * not locked and pointer interactions outside of it remain enabled.
+ * @default true
+ */
+ modal?: boolean | 'trap-focus';
+ /**
+ * Event handler called when the dialog is opened or closed.
+ */
+ onOpenChange?: (
+ open: boolean,
+ event: Event | undefined,
+ reason: DialogRoot.OpenChangeReason | undefined,
+ ) => void;
+ /**
+ * Event handler called after any animations complete when the dialog is opened or closed.
+ */
+ onOpenChangeComplete?: (open: boolean) => void;
+ /**
+ * Determines whether the dialog should close on outside clicks.
+ * @default true
+ */
+ dismissible?: boolean;
+ /**
+ * A ref to imperative actions.
+ * - `unmount`: When specified, the dialog will not be unmounted when closed.
+ * Instead, the `unmount` function must be called to unmount the dialog manually.
+ * Useful when the dialog's animation is controlled by an external library.
+ */
+ actionsRef?: React.RefObject;
+ };
+
+ export type Parameters = {
+ /**
+ * Callback to invoke when a nested dialog is opened.
+ */
+ onNestedDialogOpen?: (ownChildrenCount: number) => void;
+ /**
+ * Callback to invoke when a nested dialog is closed.
+ */
+ onNestedDialogClose?: () => void;
+ } & RequiredExcept<
+ SharedParameters,
+ 'open' | 'onOpenChange' | 'onOpenChangeComplete' | 'actionsRef'
+ >;
+
+ export type ReturnValue = {
+ /**
+ * The id of the description element associated with the dialog.
+ */
+ descriptionElementId: string | undefined;
+ /**
+ * Whether the dialog enters a modal state when open.
+ */
+ modal: boolean | 'trap-focus';
+ /**
+ * Number of nested dialogs that are currently open.
+ */
+ nestedOpenDialogCount: number;
+ /**
+ * Callback to invoke when a nested dialog is closed.
+ */
+ onNestedDialogClose?: () => void;
+ /**
+ * Callback to invoke when a nested dialog is opened.
+ */
+ onNestedDialogOpen?: (ownChildrenCount: number) => void;
+ /**
+ * Event handler called when the dialog is opened or closed.
+ */
+ setOpen: (
+ open: boolean,
+ event: Event | undefined,
+ reason: DialogRoot.OpenChangeReason | undefined,
+ ) => void;
+ /**
+ * Whether the dialog is currently open.
+ */
+ open: boolean;
+ /**
+ * Determines what triggered the dialog to open.
+ */
+ openMethod: TInteraction | null;
+ /**
+ * Callback to set the id of the description element associated with the dialog.
+ */
+ setDescriptionElementId: (elementId: string | undefined) => void;
+ /**
+ * Callback to set the id of the title element.
+ */
+ setTitleElementId: (elementId: string | undefined) => void;
+ /**
+ * The id of the title element associated with the dialog.
+ */
+ titleElementId: string | undefined;
+ /**
+ * Determines if the dialog should be mounted.
+ */
+ mounted: boolean;
+ /**
+ * The transition status of the dialog.
+ */
+ transitionStatus: TransitionStatus;
+ /**
+ * Resolver for the Trigger element's props.
+ */
+ triggerProps: HTMLProps;
+ /**
+ * Resolver for the Popup element's props.
+ */
+ getPopupProps: (externalProps?: HTMLProps) => HTMLProps;
+ /**
+ * Callback to register the Trigger element DOM node.
+ */
+ setTriggerElement: React.Dispatch>;
+ /**
+ * Callback to register the Popup element DOM node.
+ */
+ setPopupElement: React.Dispatch>;
+ /**
+ * The ref to the Popup element.
+ */
+ popupRef: React.RefObject;
+ /**
+ * A ref to the backdrop element.
+ */
+ backdropRef: React.RefObject;
+ /**
+ * A ref to the internal backdrop element.
+ */
+ internalBackdropRef: React.RefObject;
+ /**
+ * The Floating UI root context.
+ */
+ floatingRootContext: FloatingRootContext;
+ };
+}
diff --git a/packages/ui/uikit/headless/components/src/components/Dialog/title/DialogTitle.tsx b/packages/ui/uikit/headless/components/src/components/Dialog/title/DialogTitle.tsx
new file mode 100644
index 00000000..7d256d6d
--- /dev/null
+++ b/packages/ui/uikit/headless/components/src/components/Dialog/title/DialogTitle.tsx
@@ -0,0 +1,50 @@
+'use client';
+
+import { useIsoLayoutEffect } from '@flippo_ui/hooks';
+
+import { useHeadlessUiId, useRenderElement } from '@lib/hooks';
+
+import type { HeadlessUIComponentProps } from '@lib/types';
+
+import { useDialogRootContext } from '../root/DialogRootContext';
+
+/**
+ * A heading that labels the dialog.
+ * Renders an `` element.
+ *
+ * Documentation: [Base UI Dialog](https://base-ui.com/react/components/dialog)
+ */
+export function DialogTitle(componentProps: DialogTitle.Props) {
+ const {
+ /* eslint-disable unused-imports/no-unused-vars */
+ render,
+ className,
+ /* eslint-enable unused-imports/no-unused-vars */
+ id: idProp,
+ ref,
+ ...elementProps
+ } = componentProps;
+ const { setTitleElementId } = useDialogRootContext();
+
+ const id = useHeadlessUiId(idProp);
+
+ useIsoLayoutEffect(() => {
+ setTitleElementId(id);
+ return () => {
+ setTitleElementId(undefined);
+ };
+ }, [id, setTitleElementId]);
+
+ const element = useRenderElement('h2', componentProps, {
+ ref,
+ props: [{ id }, elementProps]
+ });
+
+ return element;
+}
+
+export namespace DialogTitle {
+ export type State = object;
+
+ export type Props = HeadlessUIComponentProps<'h2', State>;
+}
diff --git a/packages/ui/uikit/headless/components/src/components/Dialog/trigger/DialogTrigger.tsx b/packages/ui/uikit/headless/components/src/components/Dialog/trigger/DialogTrigger.tsx
new file mode 100644
index 00000000..6f120845
--- /dev/null
+++ b/packages/ui/uikit/headless/components/src/components/Dialog/trigger/DialogTrigger.tsx
@@ -0,0 +1,74 @@
+'use client';
+import * as React from 'react';
+
+import { CLICK_TRIGGER_IDENTIFIER } from '@lib/constants';
+import { useRenderElement } from '@lib/hooks';
+import { triggerOpenStateMapping } from '@lib/popupStateMapping';
+
+import type { HeadlessUIComponentProps, NativeButtonProps } from '@lib/types';
+
+import { useButton } from '../../use-button/useButton';
+import { useDialogRootContext } from '../root/DialogRootContext';
+
+/**
+ * A button that opens the dialog.
+ * Renders a `