diff --git a/app/globals.css b/app/globals.css
index 0ab89a899..7b7d09eb7 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -55,6 +55,9 @@
--shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45);
--tracking-normal: 0em;
--spacing: 0.25rem;
+ --motion-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
+ --motion-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
+ --motion-ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
}
.dark {
@@ -161,13 +164,13 @@
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
- --animate-accordion-down: accordion-down 0.2s ease-out;
- --animate-accordion-up: accordion-up 0.2s ease-out;
- --animate-collapse-down: collapse-down 0.1s ease-in-out;
- --animate-collapse-up: collapse-up 0.1s ease-in-out;
- --animate-slide-in-right: slide-in-right 0.3s ease-out;
- --animate-slide-out-right: slide-out-right 0.3s ease-out;
- --animate-fade-in: fade-in 0.3s ease-out;
+ --animate-accordion-down: accordion-down 180ms var(--motion-ease-out);
+ --animate-accordion-up: accordion-up 140ms var(--motion-ease-out);
+ --animate-collapse-down: collapse-down 160ms var(--motion-ease-out);
+ --animate-collapse-up: collapse-up 120ms var(--motion-ease-out);
+ --animate-slide-in-right: slide-in-right 220ms var(--motion-ease-drawer);
+ --animate-slide-out-right: slide-out-right 160ms var(--motion-ease-out);
+ --animate-fade-in: fade-in 180ms var(--motion-ease-out);
@keyframes accordion-down {
from {
@@ -247,32 +250,36 @@
100% {
transform: translateX(0);
}
- 25% {
- transform: translateX(10px);
+ 18%,
+ 30% {
+ transform: translateX(6px);
}
- 50% {
- transform: translateX(0);
+ 48%,
+ 60% {
+ transform: translateX(-6px);
}
- 75% {
- transform: translateX(-10px);
+ 78% {
+ transform: translateX(0);
}
}
}
@utility animate-blink {
- animation: blink 0.2s ease-in-out;
+ animation: blink 180ms var(--motion-ease-in-out);
}
/* Animation utilities for dropdown menu */
@utility animate-in {
animation-name: enter;
- animation-duration: 150ms;
+ animation-duration: var(--tw-duration, 160ms);
+ animation-timing-function: var(--tw-ease, var(--motion-ease-out));
animation-fill-mode: both;
}
@utility animate-out {
animation-name: exit;
- animation-duration: 150ms;
+ animation-duration: var(--tw-duration, 120ms);
+ animation-timing-function: var(--tw-ease, var(--motion-ease-out));
animation-fill-mode: both;
}
@@ -308,6 +315,38 @@
--enter-translate-x: 0.5rem;
}
+@utility slide-in-from-top {
+ --enter-translate-y: -100%;
+}
+
+@utility slide-in-from-bottom {
+ --enter-translate-y: 100%;
+}
+
+@utility slide-in-from-left {
+ --enter-translate-x: -100%;
+}
+
+@utility slide-in-from-right {
+ --enter-translate-x: 100%;
+}
+
+@utility slide-out-to-top {
+ --exit-translate-y: -100%;
+}
+
+@utility slide-out-to-bottom {
+ --exit-translate-y: 100%;
+}
+
+@utility slide-out-to-left {
+ --exit-translate-x: -100%;
+}
+
+@utility slide-out-to-right {
+ --exit-translate-x: 100%;
+}
+
@keyframes enter {
from {
opacity: var(--enter-opacity, 1);
@@ -335,6 +374,30 @@
}
}
+@media (prefers-reduced-motion: reduce) {
+ @keyframes enter {
+ from {
+ opacity: var(--enter-opacity, 1);
+ transform: translate(0, 0) scale(1);
+ }
+ to {
+ opacity: 1;
+ transform: translate(0, 0) scale(1);
+ }
+ }
+
+ @keyframes exit {
+ from {
+ opacity: 1;
+ transform: translate(0, 0) scale(1);
+ }
+ to {
+ opacity: var(--exit-opacity, 1);
+ transform: translate(0, 0) scale(1);
+ }
+ }
+}
+
@utility container {
margin-inline: auto;
padding-inline: 2rem;
diff --git a/components/action-buttons.tsx b/components/action-buttons.tsx
index 895c4f818..eae5fe150 100644
--- a/components/action-buttons.tsx
+++ b/components/action-buttons.tsx
@@ -174,7 +174,7 @@ export function ActionButtons({
{/* Action buttons */}
@@ -204,7 +204,7 @@ export function ActionButtons({
{/* Prompt samples */}
@@ -214,8 +214,8 @@ export function ActionButtons({
key={index}
type="button"
className={cn(
- 'w-full text-left px-3 py-2 rounded-md text-sm',
- 'hover:bg-muted transition-colors',
+ 'w-full rounded-md px-3 py-2 text-left text-sm',
+ 'transition-colors duration-[140ms] ease-[var(--motion-ease-out)] hover:bg-muted',
'flex items-center gap-2 group'
)}
onClick={() => handlePromptClick(prompt)}
diff --git a/components/artifact/artifact-context.tsx b/components/artifact/artifact-context.tsx
index 113468961..a21587085 100644
--- a/components/artifact/artifact-context.tsx
+++ b/components/artifact/artifact-context.tsx
@@ -13,8 +13,8 @@ import type { Part } from '@/lib/types/ai'
import { useSidebar } from '../ui/sidebar'
-// Animation duration should match CSS transition duration
-const ANIMATION_DURATION = 300
+// Animation duration should match the inspector panel exit transition.
+const ANIMATION_DURATION = 260
interface ArtifactState {
part: Part | null
diff --git a/components/artifact/chat-artifact-container.tsx b/components/artifact/chat-artifact-container.tsx
index b9ce5dfd8..dbd068ed7 100644
--- a/components/artifact/chat-artifact-container.tsx
+++ b/components/artifact/chat-artifact-container.tsx
@@ -132,7 +132,7 @@ export function ChatArtifactContainer({
return (
-
+
{hasUser && (!open || isMobileSidebar) && (
)}
@@ -164,7 +164,8 @@ export function ChatArtifactContainer({
className={cn(
'bg-background overflow-hidden',
state.isOpen && state.part ? 'opacity-100' : 'w-0 opacity-0',
- !isResizing && 'transition-all duration-300 ease-out'
+ !isResizing &&
+ 'transition-[opacity,width] duration-[260ms] ease-[var(--motion-ease-in-out)]'
)}
style={{
width: state.isOpen && state.part ? `${width}px` : '0px'
diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx
index 57f1ec3b2..436ce1e57 100644
--- a/components/chat-panel.tsx
+++ b/components/chat-panel.tsx
@@ -269,7 +269,7 @@ export function ChatPanel({
{messages.length > 0 && (
0 && (
-
+
)}