← Files NGS Analysis WorkbenchARCHIVED FILE

src/design-system/useDialogMotion.ts

1.51 KB · Sep 30, 2026 · 23:20 UTC

↓ Download file

import { useCallback, useEffect, useRef, type RefObject } from "react";

/** Keep the native modal/focus trap until its short exit has finished. */
export function useDialogMotion(
  dialogRef: RefObject<HTMLDialogElement | null>,
) {
  const exitAnimation = useRef<Animation | null>(null);
  useEffect(
    () => () => {
      exitAnimation.current?.cancel();
    },
    [],
  );

  return useCallback(() => {
    const dialog = dialogRef.current;
    if (!dialog?.open || exitAnimation.current) return;
    const style = getComputedStyle(dialog);
    const durationToken = style.getPropertyValue("--oai-duration-fast").trim();
    // Minifiers may normalize 140ms to .14s; Web Animations expects milliseconds.
    const duration =
      (parseFloat(durationToken) || 0) *
      (durationToken.endsWith("ms") ? 1 : 1000);
    if (!duration || matchMedia("(prefers-reduced-motion: reduce)").matches) {
      dialog.close();
      return;
    }
    const animation = dialog.animate(
      [
        { opacity: 1, transform: "scale(1)" },
        { opacity: 0, transform: "scale(0.98)" },
      ],
      {
        duration,
        easing: style.getPropertyValue("--oai-ease-exit").trim(),
        fill: "forwards",
      },
    );
    exitAnimation.current = animation;
    void animation.finished
      .then(
        () => {
          if (dialog.isConnected && dialog.open) dialog.close();
        },
        () => {},
      )
      .finally(() => {
        animation.cancel();
        exitAnimation.current = null;
      });
  }, [dialogRef]);
}

SHA-256: e1bd6934a945a37500b69faa9c7565b61e41a80d6b9b1463b8e8bf73543ecd09