diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b9a92a..81cae77 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@ All notable changes to `@neynar/ui` will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.2.1] - 2025-01-20
+
+### Added
+- **ResizablePanel**: Added `duration` prop (default 400ms) and `--resizable-easing` CSS variable for animation customization.
+
+---
+
## [1.2.0] - 2025-01-20
### Added
diff --git a/llm/components/resizable.llm.md b/llm/components/resizable.llm.md
index fcb94b7..928a697 100644
--- a/llm/components/resizable.llm.md
+++ b/llm/components/resizable.llm.md
@@ -72,6 +72,7 @@ All panels inherit props from react-resizable-panels Panel component.
| collapsedSize | number \| string | 0 | Size when collapsed |
| collapsed | boolean | - | Controlled collapsed state. Panel syncs to this value. |
| animated | boolean | false | Enable smooth CSS transition for collapse/expand. Drag resizing stays instant. |
+| duration | number | 400 | Animation duration in milliseconds. Only applies when `animated` is true. |
| onResize | (size: { asPercentage: number; inPixels: number }) => void | - | Called when panel is resized |
| onCollapse | () => void | - | Called when panel collapses |
| onExpand | () => void | - | Called when panel expands |
@@ -230,6 +231,22 @@ function MonitoredPanels() {
Use `collapsed` prop for declarative control and `animated` for smooth transitions. Drag resizing remains instant (no animation lag).
+**Customization:**
+- `duration` prop controls speed (default 400ms)
+- `--resizable-easing` CSS variable controls easing curve (default `cubic-bezier(0.16, 1, 0.3, 1)`)
+
+```tsx
+// Custom duration
+
+
+// Custom easing via Tailwind arbitrary property
+
+```
+
```tsx
import { useState } from "react"
import {
diff --git a/package.json b/package.json
index ad4bb48..27670a2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@neynar/ui",
- "version": "1.2.0",
+ "version": "1.2.1",
"license": "MIT",
"author": "Neynar Inc.",
"description": "AI-first React component library for coding agents. LLM-optimized docs, sensible defaults, zero config. Built on shadcn patterns, Base UI, and Tailwind CSS v4.",
diff --git a/src/components/ui/resizable.tsx b/src/components/ui/resizable.tsx
index 8c147ca..aed49cc 100644
--- a/src/components/ui/resizable.tsx
+++ b/src/components/ui/resizable.tsx
@@ -10,13 +10,17 @@ import {
import { cn } from "@/lib/utils";
-/** Duration of the collapse/expand animation in milliseconds. */
-const TRANSITION_DURATION = 300;
+/** Default duration of the collapse/expand animation in milliseconds. */
+const DEFAULT_DURATION = 400;
-/** CSS transition value for animated collapse/expand. */
-const TRANSITION_STYLE =
- "flex-grow 0.3s cubic-bezier(0.16, 1, 0.3, 1), flex-basis 0.3s cubic-bezier(0.16, 1, 0.3, 1)";
+/** Default easing function for collapse/expand animations. */
+const DEFAULT_EASING = "cubic-bezier(0.16, 1, 0.3, 1)";
+/** Generate CSS transition value for animated collapse/expand. */
+function getTransitionStyle(durationMs: number, easing: string): string {
+ const seconds = durationMs / 1000;
+ return `flex-grow ${seconds}s ${easing}, flex-basis ${seconds}s ${easing}`;
+}
type ResizablePanelGroupProps = React.ComponentProps;
@@ -48,6 +52,8 @@ type ResizablePanelProps = Omit<
animated?: boolean;
/** Controlled collapsed state. When provided, the panel syncs to this value. */
collapsed?: boolean;
+ /** Animation duration in milliseconds. Only applies when `animated` is true. @default 400 */
+ duration?: number;
};
/**
@@ -64,17 +70,20 @@ function ResizablePanel({
className,
animated,
collapsed,
+ duration = DEFAULT_DURATION,
...props
}: ResizablePanelProps) {
const panelRef = React.useRef(null);
const elementRef = React.useRef(null);
const animatedRef = React.useRef(animated);
+ const durationRef = React.useRef(duration);
const isFirstRender = React.useRef(true);
- // Keep the ref in sync with prop changes
+ // Keep the refs in sync with prop changes
React.useEffect(() => {
animatedRef.current = animated;
- }, [animated]);
+ durationRef.current = duration;
+ }, [animated, duration]);
/** Apply transition, call action, then remove transition after duration */
const withTransition = React.useCallback(
@@ -84,11 +93,17 @@ function ResizablePanel({
) as HTMLElement | null;
if (animatedRef.current && panelEl && !skipAnimation) {
- panelEl.style.transition = TRANSITION_STYLE;
+ const ms = durationRef.current;
+ // Read easing from CSS variable, fallback to default
+ const easing =
+ getComputedStyle(panelEl)
+ .getPropertyValue("--resizable-easing")
+ .trim() || DEFAULT_EASING;
+ panelEl.style.transition = getTransitionStyle(ms, easing);
action();
setTimeout(() => {
panelEl.style.transition = "";
- }, TRANSITION_DURATION);
+ }, ms);
} else {
action();
}
diff --git a/src/styles/styles.css b/src/styles/styles.css
index 1ff851d..5eaec07 100644
--- a/src/styles/styles.css
+++ b/src/styles/styles.css
@@ -73,6 +73,10 @@
body {
@apply font-sans bg-background text-foreground;
}
+ :root {
+ /* Resizable panel animation easing - override in theme or component */
+ --resizable-easing: cubic-bezier(0.16, 1, 0.3, 1);
+ }
}
/* Surface blur effect - value comes from theme's --surface-blur variable */