Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions llm/components/resizable.llm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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
<ResizablePanel animated duration={200} collapsed={isCollapsed} />

// Custom easing via Tailwind arbitrary property
<ResizablePanel
className="[--resizable-easing:ease-out]"
animated
collapsed={isCollapsed}
/>
```

```tsx
import { useState } from "react"
import {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down
33 changes: 24 additions & 9 deletions src/components/ui/resizable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof PanelGroup>;

Expand Down Expand Up @@ -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;
};

/**
Expand All @@ -64,17 +70,20 @@ function ResizablePanel({
className,
animated,
collapsed,
duration = DEFAULT_DURATION,
...props
}: ResizablePanelProps) {
const panelRef = React.useRef<PanelImperativeHandle>(null);
const elementRef = React.useRef<HTMLDivElement>(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(
Expand All @@ -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();
}
Expand Down
4 changes: 4 additions & 0 deletions src/styles/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down