From d0e8fbd5a637a8f9492c8aa07155dc0a694a6f23 Mon Sep 17 00:00:00 2001
From: Josh Perez <60019601+josh-signal@users.noreply.github.com>
Date: Thu, 14 Oct 2021 12:52:42 -0400
Subject: [PATCH] Animates ModalHost overlay

---
 package.json                          |   2 +-
 stylesheets/_modules.scss             |  16 +-
 ts/components/ConfirmationDialog.tsx  |  22 +-
 ts/components/ForwardMessageModal.tsx | 370 +++++++++++++-------------
 ts/components/Lightbox.tsx            |   6 +
 ts/components/Modal.tsx               |  33 ++-
 ts/components/ModalHost.tsx           |  40 ++-
 ts/hooks/useAnimated.tsx              |  73 +++--
 yarn.lock                             |  70 ++---
 9 files changed, 340 insertions(+), 292 deletions(-)

diff --git a/package.json b/package.json
index 70e6bc471e6..07d91613a93 100644
--- a/package.json
+++ b/package.json
@@ -73,7 +73,7 @@
   },
   "dependencies": {
     "@popperjs/core": "2.9.2",
-    "@react-spring/web": "9.2.4",
+    "@react-spring/web": "9.2.6",
     "@signalapp/signal-client": "0.9.5",
     "@sindresorhus/is": "0.8.0",
     "abort-controller": "3.0.0",
diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss
index 084dd35dabe..f732bcf34b4 100644
--- a/stylesheets/_modules.scss
+++ b/stylesheets/_modules.scss
@@ -8725,22 +8725,26 @@ button.module-image__border-overlay:focus {
 
 .module-modal-host__overlay {
   background: $color-black-alpha-40;
-  position: absolute;
-
   height: 100vh;
-  width: 100vw;
-
   left: 0;
+  position: absolute;
   top: 0;
-
+  width: 100vw;
   z-index: 2;
+}
 
+.module-modal-host__container {
   display: flex;
   flex-direction: column;
+  height: 100vh;
   justify-content: center;
-
+  left: 0;
   overflow: hidden;
   padding: 20px;
+  position: absolute;
+  top: 0;
+  width: 100vw;
+  z-index: 2;
 }
 
 // Module: GroupV2 Join Dialog
diff --git a/ts/components/ConfirmationDialog.tsx b/ts/components/ConfirmationDialog.tsx
index 07e34c988e4..a598cd93f21 100644
--- a/ts/components/ConfirmationDialog.tsx
+++ b/ts/components/ConfirmationDialog.tsx
@@ -2,6 +2,7 @@
 // SPDX-License-Identifier: AGPL-3.0-only
 
 import React, { MouseEvent, useCallback } from 'react';
+import { animated } from '@react-spring/web';
 import { Button, ButtonVariant } from './Button';
 import { LocalizerType } from '../types/Util';
 import { ModalHost } from './ModalHost';
@@ -63,17 +64,10 @@ export const ConfirmationDialog = React.memo(
     title,
     hasXButton,
   }: Props) => {
-    const { close, renderAnimation } = useAnimated(
-      {
-        from: { opacity: 0, transform: 'scale(0.25)' },
-        enter: { opacity: 1, transform: 'scale(1)' },
-        leave: { opacity: 0, onRest: () => onClose() },
-        config: {
-          duration: 150,
-        },
-      },
-      onClose
-    );
+    const { close, overlayStyles, modalStyles } = useAnimated(onClose, {
+      getFrom: () => ({ opacity: 0, transform: 'scale(0.25)' }),
+      getTo: isOpen => ({ opacity: isOpen ? 1 : 0, transform: 'scale(1)' }),
+    });
 
     const cancelAndClose = useCallback(() => {
       if (onCancel) {
@@ -94,8 +88,8 @@ export const ConfirmationDialog = React.memo(
     const hasActions = Boolean(actions.length);
 
     return (
-      <ModalHost onClose={close} theme={theme}>
-        {renderAnimation(
+      <ModalHost onClose={close} theme={theme} overlayStyles={overlayStyles}>
+        <animated.div style={modalStyles}>
           <ModalWindow
             hasXButton={hasXButton}
             i18n={i18n}
@@ -129,7 +123,7 @@ export const ConfirmationDialog = React.memo(
               ))}
             </Modal.ButtonFooter>
           </ModalWindow>
-        )}
+        </animated.div>
       </ModalHost>
     );
   }
diff --git a/ts/components/ForwardMessageModal.tsx b/ts/components/ForwardMessageModal.tsx
index 0de436740d7..4bbedd1c2ca 100644
--- a/ts/components/ForwardMessageModal.tsx
+++ b/ts/components/ForwardMessageModal.tsx
@@ -11,6 +11,7 @@ import React, {
 } from 'react';
 import Measure, { MeasuredComponentProps } from 'react-measure';
 import { noop } from 'lodash';
+import { animated } from '@react-spring/web';
 
 import classNames from 'classnames';
 import { AttachmentList } from './conversation/AttachmentList';
@@ -199,20 +200,16 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
     [contactLookup, selectedContacts, setSelectedContacts]
   );
 
-  const { close, renderAnimation } = useAnimated(
-    {
-      from: { opacity: 0, transform: 'translateY(48px)' },
-      enter: { opacity: 1, transform: 'translateY(0px)' },
-      leave: {
-        opacity: 0,
-        transform: 'translateY(48px)',
-      },
-      config: {
-        duration: 200,
-      },
-    },
-    onClose
-  );
+  const { close, modalStyles, overlayStyles } = useAnimated(onClose, {
+    getFrom: () => ({ opacity: 0, transform: 'translateY(48px)' }),
+    getTo: isOpen =>
+      isOpen
+        ? { opacity: 1, transform: 'translateY(0px)' }
+        : {
+            opacity: 0,
+            transform: 'translateY(48px)',
+          },
+  });
 
   const handleBackOrClose = useCallback(() => {
     if (isEditingMessage) {
@@ -265,188 +262,189 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
           {i18n('GroupV2--cannot-send')}
         </ConfirmationDialog>
       )}
-      <ModalHost onEscape={handleBackOrClose} onClose={close}>
-        {renderAnimation(
-          <div className="module-ForwardMessageModal">
-            <div
-              className={classNames('module-ForwardMessageModal__header', {
-                'module-ForwardMessageModal__header--edit': isEditingMessage,
-              })}
-            >
-              {isEditingMessage ? (
-                <button
-                  aria-label={i18n('back')}
-                  className="module-ForwardMessageModal__header--back"
-                  onClick={() => setIsEditingMessage(false)}
-                  type="button"
-                >
-                  &nbsp;
-                </button>
-              ) : (
-                <button
-                  aria-label={i18n('close')}
-                  className="module-ForwardMessageModal__header--close"
-                  onClick={close}
-                  type="button"
-                />
-              )}
-              <h1>{i18n('forwardMessage')}</h1>
-            </div>
+      <ModalHost
+        onEscape={handleBackOrClose}
+        onClose={close}
+        overlayStyles={overlayStyles}
+      >
+        <animated.div
+          className="module-ForwardMessageModal"
+          style={modalStyles}
+        >
+          <div
+            className={classNames('module-ForwardMessageModal__header', {
+              'module-ForwardMessageModal__header--edit': isEditingMessage,
+            })}
+          >
             {isEditingMessage ? (
-              <div className="module-ForwardMessageModal__main-body">
-                {linkPreview ? (
-                  <div className="module-ForwardMessageModal--link-preview">
-                    <StagedLinkPreview
-                      date={linkPreview.date || null}
-                      description={linkPreview.description || ''}
-                      domain={linkPreview.url}
-                      i18n={i18n}
-                      image={linkPreview.image}
-                      onClose={() => removeLinkPreview()}
-                      title={linkPreview.title}
-                    />
-                  </div>
-                ) : null}
-                {attachmentsToForward && attachmentsToForward.length ? (
-                  <AttachmentList
-                    attachments={attachmentsToForward}
+              <button
+                aria-label={i18n('back')}
+                className="module-ForwardMessageModal__header--back"
+                onClick={() => setIsEditingMessage(false)}
+                type="button"
+              >
+                &nbsp;
+              </button>
+            ) : (
+              <button
+                aria-label={i18n('close')}
+                className="module-ForwardMessageModal__header--close"
+                onClick={close}
+                type="button"
+              />
+            )}
+            <h1>{i18n('forwardMessage')}</h1>
+          </div>
+          {isEditingMessage ? (
+            <div className="module-ForwardMessageModal__main-body">
+              {linkPreview ? (
+                <div className="module-ForwardMessageModal--link-preview">
+                  <StagedLinkPreview
+                    date={linkPreview.date || null}
+                    description={linkPreview.description || ''}
+                    domain={linkPreview.url}
                     i18n={i18n}
-                    onCloseAttachment={(attachment: AttachmentType) => {
-                      const newAttachments = attachmentsToForward.filter(
-                        currentAttachment => currentAttachment !== attachment
-                      );
-                      setAttachmentsToForward(newAttachments);
-                    }}
+                    image={linkPreview.image}
+                    onClose={() => removeLinkPreview()}
+                    title={linkPreview.title}
                   />
-                ) : null}
-                <div className="module-ForwardMessageModal__text-edit-area">
-                  <CompositionInput
-                    clearQuotedMessage={shouldNeverBeCalled}
-                    draftText={messageBodyText}
-                    getQuotedMessage={noop}
+                </div>
+              ) : null}
+              {attachmentsToForward && attachmentsToForward.length ? (
+                <AttachmentList
+                  attachments={attachmentsToForward}
+                  i18n={i18n}
+                  onCloseAttachment={(attachment: AttachmentType) => {
+                    const newAttachments = attachmentsToForward.filter(
+                      currentAttachment => currentAttachment !== attachment
+                    );
+                    setAttachmentsToForward(newAttachments);
+                  }}
+                />
+              ) : null}
+              <div className="module-ForwardMessageModal__text-edit-area">
+                <CompositionInput
+                  clearQuotedMessage={shouldNeverBeCalled}
+                  draftText={messageBodyText}
+                  getQuotedMessage={noop}
+                  i18n={i18n}
+                  inputApi={inputApiRef}
+                  large
+                  moduleClassName="module-ForwardMessageModal__input"
+                  onEditorStateChange={(
+                    messageText,
+                    bodyRanges,
+                    caretLocation
+                  ) => {
+                    setMessageBodyText(messageText);
+                    onEditorStateChange(messageText, bodyRanges, caretLocation);
+                  }}
+                  onPickEmoji={onPickEmoji}
+                  onSubmit={forwardMessage}
+                  onTextTooLong={onTextTooLong}
+                />
+                <div className="module-ForwardMessageModal__emoji">
+                  <EmojiButton
                     i18n={i18n}
-                    inputApi={inputApiRef}
-                    large
-                    moduleClassName="module-ForwardMessageModal__input"
-                    onEditorStateChange={(
-                      messageText,
-                      bodyRanges,
-                      caretLocation
-                    ) => {
-                      setMessageBodyText(messageText);
-                      onEditorStateChange(
-                        messageText,
-                        bodyRanges,
-                        caretLocation
-                      );
-                    }}
-                    onPickEmoji={onPickEmoji}
-                    onSubmit={forwardMessage}
-                    onTextTooLong={onTextTooLong}
+                    onClose={focusTextEditInput}
+                    onPickEmoji={insertEmoji}
+                    onSetSkinTone={onSetSkinTone}
+                    recentEmojis={recentEmojis}
+                    skinTone={skinTone}
                   />
-                  <div className="module-ForwardMessageModal__emoji">
-                    <EmojiButton
-                      i18n={i18n}
-                      onClose={focusTextEditInput}
-                      onPickEmoji={insertEmoji}
-                      onSetSkinTone={onSetSkinTone}
-                      recentEmojis={recentEmojis}
-                      skinTone={skinTone}
-                    />
-                  </div>
                 </div>
               </div>
-            ) : (
-              <div className="module-ForwardMessageModal__main-body">
-                <SearchInput
-                  disabled={candidateConversations.length === 0}
-                  placeholder={i18n('contactSearchPlaceholder')}
-                  onChange={event => {
-                    setSearchTerm(event.target.value);
-                  }}
-                  ref={inputRef}
-                  value={searchTerm}
-                />
-                {candidateConversations.length ? (
-                  <Measure bounds>
-                    {({ contentRect, measureRef }: MeasuredComponentProps) => {
-                      // We disable this ESLint rule because we're capturing a bubbled
-                      // keydown event. See [this note in the jsx-a11y docs][0].
-                      //
-                      // [0]: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/c275964f52c35775208bd00cb612c6f82e42e34f/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events
-                      /* eslint-disable jsx-a11y/no-static-element-interactions */
-                      return (
-                        <div
-                          className="module-ForwardMessageModal__list-wrapper"
-                          ref={measureRef}
-                        >
-                          <ConversationList
-                            dimensions={contentRect.bounds}
-                            getRow={getRow}
-                            i18n={i18n}
-                            onClickArchiveButton={shouldNeverBeCalled}
-                            onClickContactCheckbox={(
-                              conversationId: string,
-                              disabledReason:
-                                | undefined
-                                | ContactCheckboxDisabledReason
-                            ) => {
-                              if (
-                                disabledReason !==
-                                ContactCheckboxDisabledReason.MaximumContactsSelected
-                              ) {
-                                toggleSelectedConversation(conversationId);
-                              }
-                            }}
-                            onSelectConversation={shouldNeverBeCalled}
-                            renderMessageSearchResult={() => {
-                              shouldNeverBeCalled();
-                              return <div />;
-                            }}
-                            rowCount={rowCount}
-                            shouldRecomputeRowHeights={false}
-                            showChooseGroupMembers={shouldNeverBeCalled}
-                            startNewConversationFromPhoneNumber={
-                              shouldNeverBeCalled
+            </div>
+          ) : (
+            <div className="module-ForwardMessageModal__main-body">
+              <SearchInput
+                disabled={candidateConversations.length === 0}
+                placeholder={i18n('contactSearchPlaceholder')}
+                onChange={event => {
+                  setSearchTerm(event.target.value);
+                }}
+                ref={inputRef}
+                value={searchTerm}
+              />
+              {candidateConversations.length ? (
+                <Measure bounds>
+                  {({ contentRect, measureRef }: MeasuredComponentProps) => {
+                    // We disable this ESLint rule because we're capturing a bubbled
+                    // keydown event. See [this note in the jsx-a11y docs][0].
+                    //
+                    // [0]: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/c275964f52c35775208bd00cb612c6f82e42e34f/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events
+                    /* eslint-disable jsx-a11y/no-static-element-interactions */
+                    return (
+                      <div
+                        className="module-ForwardMessageModal__list-wrapper"
+                        ref={measureRef}
+                      >
+                        <ConversationList
+                          dimensions={contentRect.bounds}
+                          getRow={getRow}
+                          i18n={i18n}
+                          onClickArchiveButton={shouldNeverBeCalled}
+                          onClickContactCheckbox={(
+                            conversationId: string,
+                            disabledReason:
+                              | undefined
+                              | ContactCheckboxDisabledReason
+                          ) => {
+                            if (
+                              disabledReason !==
+                              ContactCheckboxDisabledReason.MaximumContactsSelected
+                            ) {
+                              toggleSelectedConversation(conversationId);
                             }
-                          />
-                        </div>
-                      );
-                      /* eslint-enable jsx-a11y/no-static-element-interactions */
-                    }}
-                  </Measure>
-                ) : (
-                  <div className="module-ForwardMessageModal__no-candidate-contacts">
-                    {i18n('noContactsFound')}
-                  </div>
-                )}
-              </div>
-            )}
-            <div className="module-ForwardMessageModal__footer">
-              <div>
-                {Boolean(selectedContacts.length) &&
-                  selectedContacts.map(contact => contact.title).join(', ')}
-              </div>
-              <div>
-                {isEditingMessage || !isMessageEditable ? (
-                  <Button
-                    aria-label={i18n('ForwardMessageModal--continue')}
-                    className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--forward"
-                    disabled={!canForwardMessage}
-                    onClick={forwardMessage}
-                  />
-                ) : (
-                  <Button
-                    aria-label={i18n('forwardMessage')}
-                    className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--continue"
-                    disabled={!hasContactsSelected}
-                    onClick={() => setIsEditingMessage(true)}
-                  />
-                )}
-              </div>
+                          }}
+                          onSelectConversation={shouldNeverBeCalled}
+                          renderMessageSearchResult={() => {
+                            shouldNeverBeCalled();
+                            return <div />;
+                          }}
+                          rowCount={rowCount}
+                          shouldRecomputeRowHeights={false}
+                          showChooseGroupMembers={shouldNeverBeCalled}
+                          startNewConversationFromPhoneNumber={
+                            shouldNeverBeCalled
+                          }
+                        />
+                      </div>
+                    );
+                    /* eslint-enable jsx-a11y/no-static-element-interactions */
+                  }}
+                </Measure>
+              ) : (
+                <div className="module-ForwardMessageModal__no-candidate-contacts">
+                  {i18n('noContactsFound')}
+                </div>
+              )}
+            </div>
+          )}
+          <div className="module-ForwardMessageModal__footer">
+            <div>
+              {Boolean(selectedContacts.length) &&
+                selectedContacts.map(contact => contact.title).join(', ')}
+            </div>
+            <div>
+              {isEditingMessage || !isMessageEditable ? (
+                <Button
+                  aria-label={i18n('ForwardMessageModal--continue')}
+                  className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--forward"
+                  disabled={!canForwardMessage}
+                  onClick={forwardMessage}
+                />
+              ) : (
+                <Button
+                  aria-label={i18n('forwardMessage')}
+                  className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--continue"
+                  disabled={!hasContactsSelected}
+                  onClick={() => setIsEditingMessage(true)}
+                />
+              )}
             </div>
           </div>
-        )}
+        </animated.div>
       </ModalHost>
     </>
   );
diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx
index a87d0248a59..1ff7d6482a8 100644
--- a/ts/components/Lightbox.tsx
+++ b/ts/components/Lightbox.tsx
@@ -47,6 +47,12 @@ const INITIAL_IMAGE_TRANSFORM = {
   scale: 1,
   translateX: 0,
   translateY: 0,
+  config: {
+    clamp: true,
+    friction: 20,
+    mass: 0.5,
+    tension: 350,
+  },
 };
 
 export function Lightbox({
diff --git a/ts/components/Modal.tsx b/ts/components/Modal.tsx
index df16c44878d..29a014c4089 100644
--- a/ts/components/Modal.tsx
+++ b/ts/components/Modal.tsx
@@ -5,6 +5,7 @@ import React, { ReactElement, ReactNode, useRef, useState } from 'react';
 import Measure, { ContentRect, MeasuredComponentProps } from 'react-measure';
 import classNames from 'classnames';
 import { noop } from 'lodash';
+import { animated } from '@react-spring/web';
 
 import { LocalizerType } from '../types/Util';
 import { ModalHost } from './ModalHost';
@@ -42,24 +43,22 @@ export function Modal({
   title,
   theme,
 }: Readonly<ModalPropsType>): ReactElement {
-  const { close, renderAnimation } = useAnimated(
-    {
-      from: { opacity: 0, transform: 'translateY(48px)' },
-      enter: { opacity: 1, transform: 'translateY(0px)' },
-      leave: {
-        opacity: 0,
-        transform: 'translateY(48px)',
-      },
-      config: {
-        duration: 200,
-      },
-    },
-    onClose
-  );
+  const { close, modalStyles, overlayStyles } = useAnimated(onClose, {
+    getFrom: () => ({ opacity: 0, transform: 'translateY(48px)' }),
+    getTo: isOpen =>
+      isOpen
+        ? { opacity: 1, transform: 'translateY(0px)' }
+        : { opacity: 0, transform: 'translateY(48px)' },
+  });
 
   return (
-    <ModalHost noMouseClose={noMouseClose} onClose={close} theme={theme}>
-      {renderAnimation(
+    <ModalHost
+      noMouseClose={noMouseClose}
+      onClose={close}
+      overlayStyles={overlayStyles}
+      theme={theme}
+    >
+      <animated.div style={modalStyles}>
         <ModalWindow
           hasStickyButtons={hasStickyButtons}
           hasXButton={hasXButton}
@@ -70,7 +69,7 @@ export function Modal({
         >
           {children}
         </ModalWindow>
-      )}
+      </animated.div>
     </ModalHost>
   );
 }
diff --git a/ts/components/ModalHost.tsx b/ts/components/ModalHost.tsx
index 465a75becde..966dfffe151 100644
--- a/ts/components/ModalHost.tsx
+++ b/ts/components/ModalHost.tsx
@@ -5,20 +5,30 @@ import React, { useEffect } from 'react';
 import classNames from 'classnames';
 import { createPortal } from 'react-dom';
 import FocusTrap from 'focus-trap-react';
+import { SpringValues, animated } from '@react-spring/web';
 
+import type { ModalConfigType } from '../hooks/useAnimated';
 import { Theme, themeClassName } from '../util/theme';
 import { useEscapeHandling } from '../hooks/useEscapeHandling';
 
 export type PropsType = {
-  readonly noMouseClose?: boolean;
-  readonly onEscape?: () => unknown;
-  readonly onClose: () => unknown;
   readonly children: React.ReactElement;
+  readonly noMouseClose?: boolean;
+  readonly onClose: () => unknown;
+  readonly onEscape?: () => unknown;
+  readonly overlayStyles?: SpringValues<ModalConfigType>;
   readonly theme?: Theme;
 };
 
 export const ModalHost = React.memo(
-  ({ onEscape, onClose, children, noMouseClose, theme }: PropsType) => {
+  ({
+    children,
+    noMouseClose,
+    onClose,
+    onEscape,
+    theme,
+    overlayStyles,
+  }: PropsType) => {
     const [root, setRoot] = React.useState<HTMLElement | null>(null);
     const [isMouseDown, setIsMouseDown] = React.useState(false);
 
@@ -64,16 +74,18 @@ export const ModalHost = React.memo(
               allowOutsideClick: false,
             }}
           >
-            <div
-              role="presentation"
-              className={classNames(
-                'module-modal-host__overlay',
-                theme ? themeClassName(theme) : undefined
-              )}
-              onMouseDown={noMouseClose ? undefined : handleMouseDown}
-              onMouseUp={noMouseClose ? undefined : handleMouseUp}
-            >
-              {children}
+            <div>
+              <animated.div
+                role="presentation"
+                className={classNames(
+                  'module-modal-host__overlay',
+                  theme ? themeClassName(theme) : undefined
+                )}
+                onMouseDown={noMouseClose ? undefined : handleMouseDown}
+                onMouseUp={noMouseClose ? undefined : handleMouseUp}
+                style={overlayStyles}
+              />
+              <div className="module-modal-host__container">{children}</div>
             </div>
           </FocusTrap>,
           root
diff --git a/ts/hooks/useAnimated.tsx b/ts/hooks/useAnimated.tsx
index 48f484d20d2..e0d15df0962 100644
--- a/ts/hooks/useAnimated.tsx
+++ b/ts/hooks/useAnimated.tsx
@@ -1,37 +1,72 @@
 // Copyright 2021 Signal Messenger, LLC
 // SPDX-License-Identifier: AGPL-3.0-only
 
-import React, { useState, ReactElement } from 'react';
-import { animated, useTransition, UseTransitionProps } from '@react-spring/web';
-import cubicBezier from 'bezier-easing';
+import { useState } from 'react';
+import {
+  SpringValues,
+  useChain,
+  useSpring,
+  useSpringRef,
+} from '@react-spring/web';
 
-export function useAnimated<Props extends Record<string, unknown>>(
-  props: UseTransitionProps,
-  onClose: () => unknown
+export type ModalConfigType = {
+  opacity: number;
+  transform?: string;
+};
+
+export function useAnimated(
+  onClose: () => unknown,
+  {
+    getFrom,
+    getTo,
+  }: {
+    getFrom: (isOpen: boolean) => ModalConfigType;
+    getTo: (isOpen: boolean) => ModalConfigType;
+  }
 ): {
   close: () => unknown;
-  renderAnimation: (children: ReactElement) => JSX.Element;
+  modalStyles: SpringValues<ModalConfigType>;
+  overlayStyles: SpringValues<ModalConfigType>;
 } {
   const [isOpen, setIsOpen] = useState(true);
 
-  const transitions = useTransition<boolean, Props>(isOpen, {
-    ...props,
-    leave: {
-      ...props.leave,
-      onRest: () => onClose(),
+  const modalRef = useSpringRef();
+
+  const modalStyles = useSpring({
+    from: getFrom(isOpen),
+    to: getTo(isOpen),
+    onRest: () => {
+      if (!isOpen) {
+        onClose();
+      }
     },
     config: {
-      duration: 200,
-      easing: cubicBezier(0.17, 0.17, 0, 1),
-      ...props.config,
+      clamp: true,
+      friction: 20,
+      mass: 0.5,
+      tension: 350,
     },
+    ref: modalRef,
   });
 
+  const overlayRef = useSpringRef();
+
+  const overlayStyles = useSpring({
+    from: { opacity: 0 },
+    to: { opacity: isOpen ? 1 : 0 },
+    config: {
+      clamp: true,
+      friction: 22,
+      tension: 360,
+    },
+    ref: overlayRef,
+  });
+
+  useChain(isOpen ? [overlayRef, modalRef] : [modalRef, overlayRef]);
+
   return {
     close: () => setIsOpen(false),
-    renderAnimation: children =>
-      transitions((style, item) =>
-        item ? <animated.div style={style}>{children}</animated.div> : null
-      ),
+    overlayStyles,
+    modalStyles,
   };
 }
diff --git a/yarn.lock b/yarn.lock
index cf7b6535ee7..bdf01558a15 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1671,50 +1671,50 @@
     react-lifecycles-compat "^3.0.4"
     warning "^3.0.0"
 
-"@react-spring/animated@~9.2.0":
-  version "9.2.4"
-  resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.2.4.tgz#062ecc0fdfef89f2541a42d8500428b70035f879"
-  integrity sha512-AfV6ZM8pCCAT29GY5C8/1bOPjZrv/7kD0vedjiE/tEYvNDwg9GlscrvsTViWR2XykJoYrDfdkYArrldWpsCJ5g==
+"@react-spring/animated@~9.2.6-beta.0":
+  version "9.2.6"
+  resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.2.6.tgz#58f30fb75d8bfb7ccbc156cfd6b974a8f3dfd54e"
+  integrity sha512-xjL6nmixYNDvnpTs1FFMsMfSC0tURwPCU3b2jWNriYGLfwZ7c/TcyaEZA7yiNnmdFnuR3f3Z27AqIgaFC083Cw==
   dependencies:
-    "@react-spring/shared" "~9.2.0"
-    "@react-spring/types" "~9.2.0"
+    "@react-spring/shared" "~9.2.6-beta.0"
+    "@react-spring/types" "~9.2.6-beta.0"
 
-"@react-spring/core@~9.2.0":
-  version "9.2.4"
-  resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.2.4.tgz#275a4a065e3a315a4f5fb28c9a6f62ce718c25d6"
-  integrity sha512-R+PwyfsjiuYCWqaTTfCpYpRmsP0h87RNm7uxC1Uxy7QAHUfHEm2sAHn+AdHPwq/MbVwDssVT8C5yf2WGcqiXGg==
+"@react-spring/core@~9.2.6-beta.0":
+  version "9.2.6"
+  resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.2.6.tgz#ae22338fe55d070caf03abb4293b5519ba620d93"
+  integrity sha512-uPHUxmu+w6mHJrfQTMtmGJ8iZEwiVxz9kH7dRyk69bkZJt9z+w0Oj3UF4J3VcECZsbm3HRhN2ogXSAaqGjwhQw==
   dependencies:
-    "@react-spring/animated" "~9.2.0"
-    "@react-spring/shared" "~9.2.0"
-    "@react-spring/types" "~9.2.0"
+    "@react-spring/animated" "~9.2.6-beta.0"
+    "@react-spring/shared" "~9.2.6-beta.0"
+    "@react-spring/types" "~9.2.6-beta.0"
 
-"@react-spring/rafz@~9.2.0":
-  version "9.2.4"
-  resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.2.4.tgz#44793e9adc14dd0dcd1573d094368af11a89d73a"
-  integrity sha512-SOKf9eue+vAX+DGo7kWYNl9i9J3gPUlQjifIcV9Bzw9h3i30wPOOP0TjS7iMG/kLp2cdHQYDNFte6nt23VAZkQ==
+"@react-spring/rafz@~9.2.6-beta.0":
+  version "9.2.6"
+  resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.2.6.tgz#d97484003875bf5fb5e6ec22dee97cc208363e48"
+  integrity sha512-62SivLKEpo7EfHPkxO5J3g9Cr9LF6+1A1RVOMJhkcpEYtbdbmma/d63Xp8qpMPEpk7uuWxaTb6jjyxW33pW3sg==
 
-"@react-spring/shared@~9.2.0":
-  version "9.2.4"
-  resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.2.4.tgz#f9cc66ac5308a77293330a18518e34121f4008c1"
-  integrity sha512-ZEr4l2BxmyFRUvRA2VCkPfCJii4E7cGkwbjmTBx1EmcGrOnde/V2eF5dxqCTY3k35QuCegkrWe0coRJVkh8q2Q==
+"@react-spring/shared@~9.2.6-beta.0":
+  version "9.2.6"
+  resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.2.6.tgz#2c84e62cc0cfbbbbeb5546acd46c1f4b248bc562"
+  integrity sha512-Qrm9fopKG/RxZ3Rw+4euhrpnB3uXSyiON9skHbcBfmkkzagpkUR66MX1YLrhHw0UchcZuSDnXs0Lonzt1rpWag==
   dependencies:
-    "@react-spring/rafz" "~9.2.0"
-    "@react-spring/types" "~9.2.0"
+    "@react-spring/rafz" "~9.2.6-beta.0"
+    "@react-spring/types" "~9.2.6-beta.0"
 
-"@react-spring/types@~9.2.0":
-  version "9.2.4"
-  resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.2.4.tgz#2365ce9d761f548a9adcb2cd68714bf26765a5de"
-  integrity sha512-zHUXrWO8nweUN/ISjrjqU7GgXXvoEbFca1CgiE0TY0H/dqJb3l+Rhx8ecPVNYimzFg3ZZ1/T0egpLop8SOv4aA==
+"@react-spring/types@~9.2.6-beta.0":
+  version "9.2.6"
+  resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.2.6.tgz#f60722fcf9f8492ae16d0bdc47f0ea3c2a16d2cf"
+  integrity sha512-l7mCw182DtDMnCI8CB9orgTAEoFZRtdQ6aS6YeEAqYcy3nQZPmPggIHH9DxyLw7n7vBPRSzu9gCvUMgXKpTflg==
 
-"@react-spring/web@9.2.4":
-  version "9.2.4"
-  resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.2.4.tgz#c6d5464a954bfd0d7bc90117050f796a95ebfa08"
-  integrity sha512-vtPvOalLFvuju/MDBtoSnCyt0xXSL6Amyv82fljOuWPl1yGd4M1WteijnYL9Zlriljl0a3oXcPunAVYTD9dbDQ==
+"@react-spring/web@9.2.6":
+  version "9.2.6"
+  resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.2.6.tgz#c4fba69e1b1b43bd1d6a62346530cfb07f2be09b"
+  integrity sha512-0HkRsEYR/CO3Uw46FWDWaF2wg2rUXcWE2R9AoZXthEYLUn5w9uE1mf2Jel7BxBxWGQ73owkqSQv+klA1Hb+ViQ==
   dependencies:
-    "@react-spring/animated" "~9.2.0"
-    "@react-spring/core" "~9.2.0"
-    "@react-spring/shared" "~9.2.0"
-    "@react-spring/types" "~9.2.0"
+    "@react-spring/animated" "~9.2.6-beta.0"
+    "@react-spring/core" "~9.2.6-beta.0"
+    "@react-spring/shared" "~9.2.6-beta.0"
+    "@react-spring/types" "~9.2.6-beta.0"
 
 "@signalapp/signal-client@0.9.5":
   version "0.9.5"