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 */