diff --git a/app/components/RoundedSector.tsx b/app/components/RoundedSector.tsx index abf27bcd18..960dd9f539 100644 --- a/app/components/RoundedSector.tsx +++ b/app/components/RoundedSector.tsx @@ -5,10 +5,9 @@ * * Copyright Oxide Computer Company */ +import { useReducedMotion } from 'motion/react' import { useEffect, useMemo, useState } from 'react' -import { useReducedMotion } from '~/hooks/use-reduce-motion' - export function RoundedSector({ angle, size, diff --git a/app/components/ToastStack.tsx b/app/components/ToastStack.tsx index 78fe655e69..39a95919e7 100644 --- a/app/components/ToastStack.tsx +++ b/app/components/ToastStack.tsx @@ -5,7 +5,8 @@ * * Copyright Oxide Computer Company */ -import { animated, useTransition } from '@react-spring/web' +import { AnimatePresence } from 'motion/react' +import * as m from 'motion/react-m' import { removeToast, useToastStore } from '~/stores/toast' import { Toast } from '~/ui/lib/Toast' @@ -13,37 +14,30 @@ import { Toast } from '~/ui/lib/Toast' export function ToastStack() { const toasts = useToastStore((state) => state.toasts) - const transition = useTransition(toasts, { - keys: (toast) => toast.id, - from: { opacity: 0, y: 10, scale: 95 }, - enter: { opacity: 1, y: 0, scale: 100 }, - leave: { opacity: 0, y: 10, scale: 95 }, - config: { duration: 100 }, - }) - return (
- {transition((style, item) => ( - `scale(${val}%, ${val}%)`), - }} - > - { - removeToast(item.id) - item.options.onClose?.() - }} - /> - - ))} + + {toasts.map((toast) => ( + + { + removeToast(toast.id) + toast.options.onClose?.() + }} + /> + + ))} +
) } diff --git a/app/hooks/use-reduce-motion.tsx b/app/hooks/use-reduce-motion.tsx deleted file mode 100644 index e5db389868..0000000000 --- a/app/hooks/use-reduce-motion.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -import { Globals } from '@react-spring/web' -import { useEffect, useState } from 'react' - -Globals.assign({ skipAnimation: true }) - -const motionQuery = () => window.matchMedia('(prefers-reduced-motion: reduce)') - -/** - * Pulled from [react-reduce-motion](https://github.com/infiniteluke/react-reduce-motion). - */ -export function useReducedMotion() { - const [reducedMotion, setReducedMotion] = useState(motionQuery().matches) - useEffect(() => { - const mq = motionQuery() - const handleChange = () => setReducedMotion(mq.matches) - handleChange() - mq.addEventListener('change', handleChange) - return () => mq.removeEventListener('change', handleChange) - }, []) - return reducedMotion -} - -export function ReduceMotion() { - const prefersReducedMotion = useReducedMotion() - - useEffect(() => { - Globals.assign({ skipAnimation: prefersReducedMotion }) - }, [prefersReducedMotion]) - - return null -} diff --git a/app/main.tsx b/app/main.tsx index 1db612059e..6bc7ecb541 100644 --- a/app/main.tsx +++ b/app/main.tsx @@ -6,6 +6,7 @@ * Copyright Oxide Computer Company */ import { QueryClientProvider } from '@tanstack/react-query' +import { LazyMotion, MotionConfig } from 'motion/react' // import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' @@ -16,7 +17,6 @@ import { queryClient } from '@oxide/api' import { ConfirmActionModal } from './components/ConfirmActionModal' import { ErrorBoundary } from './components/ErrorBoundary' -import { ReduceMotion } from './hooks/use-reduce-motion' // stripped out by rollup in production import { startMockAPI } from './msw-mock-api' import { routes } from './routes' @@ -33,6 +33,8 @@ if (process.env.SHA) { ) } +const loadFeatures = () => import('./util/motion-features').then((res) => res.domAnimation) + const root = createRoot(document.getElementById('root')!) function render() { @@ -46,12 +48,15 @@ function render() { root.render( - - - - - - + + + + + + + + + {/* */} diff --git a/app/ui/lib/Button.tsx b/app/ui/lib/Button.tsx index d77c893b6d..b5a3190fe7 100644 --- a/app/ui/lib/Button.tsx +++ b/app/ui/lib/Button.tsx @@ -6,6 +6,7 @@ * Copyright Oxide Computer Company */ import cn from 'classnames' +import * as m from 'motion/react-m' import { forwardRef, type MouseEventHandler, type ReactNode } from 'react' import { Spinner } from '~/ui/lib/Spinner' @@ -90,9 +91,14 @@ export const Button = forwardRef( with={} > ) diff --git a/app/ui/lib/CopyToClipboard.tsx b/app/ui/lib/CopyToClipboard.tsx index da07926d4e..df0331f531 100644 --- a/app/ui/lib/CopyToClipboard.tsx +++ b/app/ui/lib/CopyToClipboard.tsx @@ -6,8 +6,9 @@ * Copyright Oxide Computer Company */ -import { animated, config, useTransition } from '@react-spring/web' import cn from 'classnames' +import { AnimatePresence } from 'motion/react' +import * as m from 'motion/react-m' import { useState } from 'react' import { Copy12Icon, Success12Icon } from '@oxide/design-system/icons/react' @@ -20,6 +21,11 @@ type Props = { className?: string } +const variants = { + hidden: { opacity: 0, scale: 0.75 }, + visible: { opacity: 1, scale: 1 }, +} + export const CopyToClipboard = ({ ariaLabel = 'Click to copy', text, @@ -35,14 +41,14 @@ export const CopyToClipboard = ({ }) } - const transitions = useTransition(hasCopied, { - from: { opacity: 0, transform: 'scale(0.8)' }, - enter: { opacity: 1, transform: 'scale(1)' }, - leave: { opacity: 0, transform: 'scale(0.8)' }, - config: config.stiff, - trail: 100, - initial: null, - }) + const animateProps = { + className: 'absolute inset-0 flex items-center justify-center', + variants, + initial: 'hidden', + animate: 'visible', + exit: 'hidden', + transition: { type: 'spring', duration: 0.2, bounce: 0 }, + } return ( ) } diff --git a/app/ui/lib/DialogOverlay.tsx b/app/ui/lib/DialogOverlay.tsx index 005ebac86f..4fbe391925 100644 --- a/app/ui/lib/DialogOverlay.tsx +++ b/app/ui/lib/DialogOverlay.tsx @@ -6,12 +6,17 @@ * Copyright Oxide Computer Company */ +import * as m from 'motion/react-m' import { forwardRef } from 'react' export const DialogOverlay = forwardRef((_, ref) => ( -
)) diff --git a/app/ui/lib/Modal.tsx b/app/ui/lib/Modal.tsx index 261300f6ac..830a7fc089 100644 --- a/app/ui/lib/Modal.tsx +++ b/app/ui/lib/Modal.tsx @@ -6,8 +6,8 @@ * Copyright Oxide Computer Company */ import * as Dialog from '@radix-ui/react-dialog' -import { animated, useTransition } from '@react-spring/web' import cn from 'classnames' +import * as m from 'motion/react-m' import type { MergeExclusive } from 'type-fest' import { Close12Icon } from '@oxide/design-system/icons/react' @@ -41,63 +41,49 @@ export function Modal({ narrow, overlay = true, }: ModalProps) { - const AnimatedDialogContent = animated(Dialog.Content) - - const config = { tension: 650, mass: 0.125 } - - const transitions = useTransition(isOpen, { - from: { y: -5 }, - enter: { y: 0 }, - config: isOpen ? config : { duration: 0 }, - }) - return ( - {transitions( - ({ y }, item) => - item && ( - { - if (!open) onDismiss() - }} - // https://github.com/radix-ui/primitives/issues/1159#issuecomment-1559813266 - modal={false} + { + if (!open) onDismiss() + }} + modal={false} + > + + {overlay && } + e.preventDefault()} + aria-describedby={undefined} // radix warns without this + > + - - {overlay && } - - `translate3d(-50%, ${-50 + value}%, 0px)`), - }} - // Prevents cancel loop on clicking on background over side - // modal to get out of image upload modal. Canceling out of - // confirm dialog returns focus to the dismissable layer, - // which triggers onDismiss again. And again. - // https://github.com/oxidecomputer/console/issues/1745 - onFocusOutside={(e) => e.preventDefault()} - > - - {title} - - {children} - - - - - - - ) - )} + + {title} + + {children} + + + + + + + ) } diff --git a/app/ui/lib/SideModal.tsx b/app/ui/lib/SideModal.tsx index 01aae0f70c..5f1fe56603 100644 --- a/app/ui/lib/SideModal.tsx +++ b/app/ui/lib/SideModal.tsx @@ -6,8 +6,8 @@ * Copyright Oxide Computer Company */ import * as Dialog from '@radix-ui/react-dialog' -import { animated, useTransition } from '@react-spring/web' import cn from 'classnames' +import * as m from 'motion/react-m' import React, { useRef, type ReactNode } from 'react' import { Close12Icon, Error12Icon } from '@oxide/design-system/icons/react' @@ -49,85 +49,69 @@ export function SideModal({ onDismiss, title, subtitle, - isOpen, animate = true, errors, }: SideModalProps) { - const AnimatedDialogContent = animated(Dialog.Content) - - const config = { tension: 650, mass: 0.125 } - - const transitions = useTransition(isOpen, { - from: { x: 50 }, - enter: { x: 0 }, - config: isOpen && animate ? config : { duration: 0 }, - }) - return ( - {transitions( - ({ x }, item) => - item && ( - { - if (!open) onDismiss() - }} - // https://github.com/radix-ui/primitives/issues/1159#issuecomment-1559813266 - modal={false} + { + if (!open) onDismiss() + }} + // https://github.com/radix-ui/primitives/issues/1159#issuecomment-1559813266 + modal={false} + > + + + + - - - `translate3d(${value}%, 0px, 0px)`), - }} - // shuts off a warning from radix about dialog content needing a description - aria-describedby={undefined} - > -
- - {title} - - {subtitle} -
- {errors && errors.length > 0 && ( -
- -
{errors.length} issues:
-
    - {errors.map((error, idx) => ( -
  • {error}
  • - ))} -
- - ) - } - title={errors.length > 1 ? 'Errors' : 'Error'} - /> -
- )} - {children} - - {/* Close button is here at the end so we aren't automatically focusing on it when the side modal is opened. Positioned in the safe area at the top */} - - - -
-
-
- ) - )} +
+ + {title} + + {subtitle} +
+ {errors && errors.length > 0 && ( +
+ +
{errors.length} issues:
+
    + {errors.map((error, idx) => ( +
  • {error}
  • + ))} +
+ + ) + } + title={errors.length > 1 ? 'Errors' : 'Error'} + /> +
+ )} + {children} + + {/* Close button is here at the end so we aren't automatically focusing on it when the side modal is opened. Positioned in the safe area at the top */} + + + + + + +
) } diff --git a/app/ui/lib/TimeoutIndicator.tsx b/app/ui/lib/TimeoutIndicator.tsx index ca19c20e2e..e2041982d9 100644 --- a/app/ui/lib/TimeoutIndicator.tsx +++ b/app/ui/lib/TimeoutIndicator.tsx @@ -5,38 +5,16 @@ * * Copyright Oxide Computer Company */ -import { animated, Globals, useTransition } from '@react-spring/web' -import cn from 'classnames' import { useTimeout } from './use-timeout' export interface TimeoutIndicatorProps { timeout: number onTimeoutEnd: () => void - className: string } -export const TimeoutIndicator = ({ - timeout, - onTimeoutEnd, - className, -}: TimeoutIndicatorProps) => { - const transitions = useTransition(true, { - from: { width: '0%' }, - enter: { width: '100%' }, - leave: { width: '100%' }, - config: { duration: timeout }, - }) - +export const TimeoutIndicator = ({ timeout, onTimeoutEnd }: TimeoutIndicatorProps) => { useTimeout(onTimeoutEnd, timeout) - // Don't show progress bar if reduce motion is turned on - if (Globals.skipAnimation) return null - - return transitions((styles) => ( - - )) + return null } diff --git a/app/ui/lib/Toast.tsx b/app/ui/lib/Toast.tsx index 62ea00a43a..8c0a727cd8 100644 --- a/app/ui/lib/Toast.tsx +++ b/app/ui/lib/Toast.tsx @@ -66,12 +66,6 @@ const secondaryTextColor: Record = { info: 'text-notice-secondary', } -const progressColor: Record = { - success: 'bg-accent-raise', - error: 'bg-destructive-raise', - info: 'bg-notice-raise', -} - export const Toast = ({ title, content, @@ -90,7 +84,7 @@ export const Toast = ({ return (
)}
- +
+ +
- {timeout !== null && ( - - )} + {timeout !== null && }
) } diff --git a/app/ui/styles/components/menu-button.css b/app/ui/styles/components/menu-button.css index 849902121c..b9f3617b19 100644 --- a/app/ui/styles/components/menu-button.css +++ b/app/ui/styles/components/menu-button.css @@ -45,7 +45,7 @@ .DropdownMenuContent, .DocsPopoverPanel { - animation: slide-down 0.2s ease; + animation: slide-down 0.2s var(--ease-out-quad); } @media (prefers-reduced-motion) { diff --git a/app/ui/styles/index.css b/app/ui/styles/index.css index 4862fece0a..52adedc6e7 100644 --- a/app/ui/styles/index.css +++ b/app/ui/styles/index.css @@ -45,6 +45,28 @@ :root { --content-gutter: 2.5rem; --top-bar-height: 54px; + + /* Nicer easing from: https://twitter.com/bdc */ + --ease-in-quad: cubic-bezier(0.55, 0.085, 0.68, 0.53); + --ease-in-cubic: cubic-bezier(0.55, 0.055, 0.675, 0.19); + --ease-in-quart: cubic-bezier(0.895, 0.03, 0.685, 0.22); + --ease-in-quint: cubic-bezier(0.755, 0.05, 0.855, 0.06); + --ease-in-expo: cubic-bezier(0.95, 0.05, 0.795, 0.035); + --ease-in-circ: cubic-bezier(0.6, 0.04, 0.98, 0.335); + + --ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94); + --ease-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1); + --ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1); + --ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1); + --ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1); + --ease-out-circ: cubic-bezier(0.075, 0.82, 0.165, 1); + + --ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955); + --ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1); + --ease-in-out-quart: cubic-bezier(0.77, 0, 0.175, 1); + --ease-in-out-quint: cubic-bezier(0.86, 0, 0.07, 1); + --ease-in-out-expo: cubic-bezier(1, 0, 0, 1); + --ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.15, 0.86); } @layer base { @@ -117,6 +139,22 @@ input[type='number']:focus-visible { } } +a, +button, +.ox-tabs-panel, +[role='listbox'], +[role='option'], +[role='button'], +input[type='text'], +input[type='textarea'], +textarea[type='text'], +input[type='file'], +input[type='radio'], +input[type='checkbox'], +input[type='number'] { + @apply transition-[outline-width] duration-100 ease-out; +} + a:focus, button:focus, .ox-tabs-panel:focus, diff --git a/app/util/motion-features.ts b/app/util/motion-features.ts new file mode 100644 index 0000000000..82b13deb9b --- /dev/null +++ b/app/util/motion-features.ts @@ -0,0 +1,10 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +// see https://motion.dev/docs/react-reduce-bundle-size#lazy-loading +export { domAnimation } from 'motion/react' diff --git a/package-lock.json b/package-lock.json index 5159e7bc15..b0b9947dd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "@radix-ui/react-focus-guards": "1.0.1", "@radix-ui/react-tabs": "^1.1.0", "@react-aria/live-announcer": "^3.3.4", - "@react-spring/web": "^9.7.4", "@tanstack/react-query": "^5.56.2", "@tanstack/react-query-devtools": "^5.56.2", "@tanstack/react-table": "^8.20.5", @@ -30,6 +29,7 @@ "lodash.throttle": "^4.1.1", "match-sorter": "^6.3.4", "md5": "^2.3.0", + "motion": "^11.16.2", "mousetrap": "^1.6.5", "p-map": "^7.0.2", "p-retry": "^6.2.0", @@ -3307,78 +3307,6 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@react-spring/animated": { - "version": "9.7.4", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.4.tgz", - "integrity": "sha512-7As+8Pty2QlemJ9O5ecsuPKjmO0NKvmVkRR1n6mEotFgWar8FKuQt2xgxz3RTgxcccghpx1YdS1FCdElQNexmQ==", - "license": "MIT", - "dependencies": { - "@react-spring/shared": "~9.7.4", - "@react-spring/types": "~9.7.4" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/core": { - "version": "9.7.4", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.4.tgz", - "integrity": "sha512-GzjA44niEJBFUe9jN3zubRDDDP2E4tBlhNlSIkTChiNf9p4ZQlgXBg50qbXfSXHQPHak/ExYxwhipKVsQ/sUTw==", - "license": "MIT", - "dependencies": { - "@react-spring/animated": "~9.7.4", - "@react-spring/shared": "~9.7.4", - "@react-spring/types": "~9.7.4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-spring/donate" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/rafz": { - "version": "9.7.4", - "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.4.tgz", - "integrity": "sha512-mqDI6rW0Ca8IdryOMiXRhMtVGiEGLIO89vIOyFQXRIwwIMX30HLya24g9z4olDvFyeDW3+kibiKwtZnA4xhldA==", - "license": "MIT" - }, - "node_modules/@react-spring/shared": { - "version": "9.7.4", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.4.tgz", - "integrity": "sha512-bEPI7cQp94dOtCFSEYpxvLxj0+xQfB5r9Ru1h8OMycsIq7zFZon1G0sHrBLaLQIWeMCllc4tVDYRTLIRv70C8w==", - "license": "MIT", - "dependencies": { - "@react-spring/rafz": "~9.7.4", - "@react-spring/types": "~9.7.4" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/types": { - "version": "9.7.4", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.4.tgz", - "integrity": "sha512-iQVztO09ZVfsletMiY+DpT/JRiBntdsdJ4uqk3UJFhrhS8mIC9ZOZbmfGSRs/kdbNPQkVyzucceDicQ/3Mlj9g==", - "license": "MIT" - }, - "node_modules/@react-spring/web": { - "version": "9.7.4", - "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.4.tgz", - "integrity": "sha512-UMvCZp7I5HCVIleSa4BwbNxynqvj+mJjG2m20VO2yPoi2pnCYANy58flvz9v/YcXTAvsmL655FV3pm5fbr6akA==", - "license": "MIT", - "dependencies": { - "@react-spring/animated": "~9.7.4", - "@react-spring/core": "~9.7.4", - "@react-spring/shared": "~9.7.4", - "@react-spring/types": "~9.7.4" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/@react-stately/calendar": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.5.4.tgz", @@ -8761,6 +8689,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "11.16.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.16.2.tgz", + "integrity": "sha512-M946d8UhmI4lVZ4Wy2bLxw7D7LWw+OZTK5eCFCpGJNpUKt17oCP7+bBM3iKp6PfJF30ngBxsdxssFjLdD85ThA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.16.1", + "motion-utils": "^11.16.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -10750,6 +10705,47 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/motion": { + "version": "11.16.2", + "resolved": "https://registry.npmjs.org/motion/-/motion-11.16.2.tgz", + "integrity": "sha512-Q73vRcFCLTfKdIq8CllBi72zvntKEnaFaE3Wh0y0cWxeQUAw7VymVg8eZpLADZku7SNvk4GhZJqnIVl8eGepiw==", + "license": "MIT", + "dependencies": { + "framer-motion": "^11.16.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "11.16.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.16.1.tgz", + "integrity": "sha512-XVNf3iCfZn9OHPZYJQy5YXXLn0NuPNvtT3YCat89oAnr4D88Cr52KqFgKa8dWElBK8uIoQhpJMJEG+dyniYycQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.16.0" + } + }, + "node_modules/motion-utils": { + "version": "11.16.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.16.0.tgz", + "integrity": "sha512-ngdWPjg31rD4WGXFi0eZ00DQQqKKu04QExyv/ymlC+3k+WIgYVFbt6gS5JsFPbJODTF/r8XiE/X+SsoT9c0ocw==", + "license": "MIT" + }, "node_modules/mousetrap": { "version": "1.6.5", "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz", diff --git a/package.json b/package.json index 4dc0dbeab8..5e8c203870 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "@radix-ui/react-focus-guards": "1.0.1", "@radix-ui/react-tabs": "^1.1.0", "@react-aria/live-announcer": "^3.3.4", - "@react-spring/web": "^9.7.4", "@tanstack/react-query": "^5.56.2", "@tanstack/react-query-devtools": "^5.56.2", "@tanstack/react-table": "^8.20.5", @@ -52,6 +51,7 @@ "lodash.throttle": "^4.1.1", "match-sorter": "^6.3.4", "md5": "^2.3.0", + "motion": "^11.16.2", "mousetrap": "^1.6.5", "p-map": "^7.0.2", "p-retry": "^6.2.0",