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={ }
>
(
aria-disabled={isDisabled}
{...rest}
>
- {loading && }
-
+ {loading && (
+
+
+
+ )}
+
{children}
-
+
)
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 (
- {transitions((styles, item) => (
-
- {item ? : }
-
- ))}
+
+ {hasCopied ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
)
}
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",