@neynar/ui 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,8 @@ type ResizablePanelProps = Omit<React.ComponentProps<typeof Panel>, "panelRef">
11
11
  animated?: boolean;
12
12
  /** Controlled collapsed state. When provided, the panel syncs to this value. */
13
13
  collapsed?: boolean;
14
+ /** Animation duration in milliseconds. Only applies when `animated` is true. @default 400 */
15
+ duration?: number;
14
16
  };
15
17
  /**
16
18
  * Individual resizable panel within a ResizablePanelGroup.
@@ -22,7 +24,7 @@ type ResizablePanelProps = Omit<React.ComponentProps<typeof Panel>, "panelRef">
22
24
  * @example
23
25
  * <ResizablePanel collapsed={isCollapsed} animated collapsible>
24
26
  */
25
- declare function ResizablePanel({ className, animated, collapsed, ...props }: ResizablePanelProps): import("react/jsx-runtime").JSX.Element;
27
+ declare function ResizablePanel({ className, animated, collapsed, duration, ...props }: ResizablePanelProps): import("react/jsx-runtime").JSX.Element;
26
28
  type ResizableHandleProps = React.ComponentProps<typeof PanelResizeHandle> & {
27
29
  /** Display a visible grip indicator on the resize handle. @default false */
28
30
  withHandle?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"resizable.d.ts","sourceRoot":"","sources":["../../../src/components/ui/resizable.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EACL,KAAK,IAAI,UAAU,EACnB,KAAK,EACL,SAAS,IAAI,iBAAiB,EAE/B,MAAM,wBAAwB,CAAC;AAYhC,KAAK,wBAAwB,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,UAAU,CAAC,CAAC;AAExE;;;GAGG;AACH,iBAAS,mBAAmB,CAAC,EAC3B,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,2CAW1B;AAED,KAAK,mBAAmB,GAAG,IAAI,CAC7B,KAAK,CAAC,cAAc,CAAC,OAAO,KAAK,CAAC,EAClC,UAAU,CACX,GAAG;IACF,oGAAoG;IACpG,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF;;;;;;;;;GASG;AACH,iBAAS,cAAc,CAAC,EACtB,SAAS,EACT,QAAQ,EACR,SAAS,EACT,GAAG,KAAK,EACT,EAAE,mBAAmB,2CAmErB;AAED,KAAK,oBAAoB,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,iBAAiB,CAAC,GAAG;IAC3E,4EAA4E;IAC5E,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,iBAAS,eAAe,CAAC,EACvB,UAAU,EACV,SAAS,EACT,GAAG,KAAK,EACT,EAAE,oBAAoB,2CAetB;AAED,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,GAC1B,CAAC"}
1
+ {"version":3,"file":"resizable.d.ts","sourceRoot":"","sources":["../../../src/components/ui/resizable.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EACL,KAAK,IAAI,UAAU,EACnB,KAAK,EACL,SAAS,IAAI,iBAAiB,EAE/B,MAAM,wBAAwB,CAAC;AAgBhC,KAAK,wBAAwB,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,UAAU,CAAC,CAAC;AAExE;;;GAGG;AACH,iBAAS,mBAAmB,CAAC,EAC3B,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,2CAW1B;AAED,KAAK,mBAAmB,GAAG,IAAI,CAC7B,KAAK,CAAC,cAAc,CAAC,OAAO,KAAK,CAAC,EAClC,UAAU,CACX,GAAG;IACF,oGAAoG;IACpG,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;;;;GASG;AACH,iBAAS,cAAc,CAAC,EACtB,SAAS,EACT,QAAQ,EACR,SAAS,EACT,QAA2B,EAC3B,GAAG,KAAK,EACT,EAAE,mBAAmB,2CAoFrB;AAED,KAAK,oBAAoB,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,iBAAiB,CAAC,GAAG;IAC3E,4EAA4E;IAC5E,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,iBAAS,eAAe,CAAC,EACvB,UAAU,EACV,SAAS,EACT,GAAG,KAAK,EACT,EAAE,oBAAoB,2CAetB;AAED,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,GAC1B,CAAC"}
@@ -3,8 +3,12 @@ import { jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Group as Tt, Panel as $t, Separator as Ht } from "../../node_modules/react-resizable-panels/dist/react-resizable-panels.js";
5
5
  import { cn } from "../../lib/utils.js";
6
- const TRANSITION_DURATION = 300;
7
- 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)";
6
+ const DEFAULT_DURATION = 400;
7
+ const DEFAULT_EASING = "cubic-bezier(0.16, 1, 0.3, 1)";
8
+ function getTransitionStyle(durationMs, easing) {
9
+ const seconds = durationMs / 1e3;
10
+ return `flex-grow ${seconds}s ${easing}, flex-basis ${seconds}s ${easing}`;
11
+ }
8
12
  function ResizablePanelGroup({
9
13
  className,
10
14
  ...props
@@ -25,26 +29,31 @@ function ResizablePanel({
25
29
  className,
26
30
  animated,
27
31
  collapsed,
32
+ duration = DEFAULT_DURATION,
28
33
  ...props
29
34
  }) {
30
35
  const panelRef = React.useRef(null);
31
36
  const elementRef = React.useRef(null);
32
37
  const animatedRef = React.useRef(animated);
33
- const isFirstRender = React.useRef(true);
38
+ const durationRef = React.useRef(duration);
39
+ const prevCollapsedRef = React.useRef(void 0);
34
40
  React.useEffect(() => {
35
41
  animatedRef.current = animated;
36
- }, [animated]);
42
+ durationRef.current = duration;
43
+ }, [animated, duration]);
37
44
  const withTransition = React.useCallback(
38
45
  (action, skipAnimation = false) => {
39
46
  const panelEl = elementRef.current?.closest(
40
47
  '[data-panel][data-slot="resizable-panel"]'
41
48
  );
42
49
  if (animatedRef.current && panelEl && !skipAnimation) {
43
- panelEl.style.transition = TRANSITION_STYLE;
50
+ const ms = durationRef.current;
51
+ const easing = getComputedStyle(panelEl).getPropertyValue("--resizable-easing").trim() || DEFAULT_EASING;
52
+ panelEl.style.transition = getTransitionStyle(ms, easing);
44
53
  action();
45
54
  setTimeout(() => {
46
55
  panelEl.style.transition = "";
47
- }, TRANSITION_DURATION);
56
+ }, ms);
48
57
  } else {
49
58
  action();
50
59
  }
@@ -53,22 +62,28 @@ function ResizablePanel({
53
62
  );
54
63
  React.useEffect(() => {
55
64
  if (collapsed === void 0) return;
56
- if (isFirstRender.current) {
57
- isFirstRender.current = false;
58
- requestAnimationFrame(() => {
65
+ const isFirstSync = prevCollapsedRef.current === void 0;
66
+ const hasChanged = collapsed !== prevCollapsedRef.current;
67
+ prevCollapsedRef.current = collapsed;
68
+ if (!hasChanged && !isFirstSync) return;
69
+ const syncState = () => {
70
+ try {
71
+ const isCurrentlyCollapsed = panelRef.current?.isCollapsed() ?? false;
72
+ if (collapsed === isCurrentlyCollapsed) return;
59
73
  if (collapsed) {
60
- panelRef.current?.collapse();
74
+ if (isFirstSync) {
75
+ panelRef.current?.collapse();
76
+ } else {
77
+ withTransition(() => panelRef.current?.collapse());
78
+ }
79
+ } else {
80
+ withTransition(() => panelRef.current?.expand());
61
81
  }
62
- });
63
- return;
64
- }
65
- const isCurrentlyCollapsed = panelRef.current?.isCollapsed() ?? false;
66
- if (collapsed === isCurrentlyCollapsed) return;
67
- if (collapsed) {
68
- withTransition(() => panelRef.current?.collapse());
69
- } else {
70
- withTransition(() => panelRef.current?.expand());
71
- }
82
+ } catch {
83
+ setTimeout(syncState, 0);
84
+ }
85
+ };
86
+ setTimeout(syncState, 0);
72
87
  }, [collapsed, withTransition]);
73
88
  return /* @__PURE__ */ jsx(
74
89
  $t,
@@ -1 +1 @@
1
- {"version":3,"file":"resizable.js","sources":["../../../src/components/ui/resizable.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport {\n Group as PanelGroup,\n Panel,\n Separator as PanelResizeHandle,\n type PanelImperativeHandle,\n} from \"react-resizable-panels\";\n\nimport { cn } from \"@/lib/utils\";\n\n/** Duration of the collapse/expand animation in milliseconds. */\nconst TRANSITION_DURATION = 300;\n\n/** CSS transition value for animated collapse/expand. */\nconst TRANSITION_STYLE =\n \"flex-grow 0.3s cubic-bezier(0.16, 1, 0.3, 1), flex-basis 0.3s cubic-bezier(0.16, 1, 0.3, 1)\";\n\n\ntype ResizablePanelGroupProps = React.ComponentProps<typeof PanelGroup>;\n\n/**\n * Container for resizable panels. Manages layout orientation and panel resizing behavior.\n * Supports both horizontal and vertical layouts with optional persistence via `id` prop.\n */\nfunction ResizablePanelGroup({\n className,\n ...props\n}: ResizablePanelGroupProps) {\n return (\n <PanelGroup\n data-slot=\"resizable-panel-group\"\n className={cn(\n \"flex h-full w-full aria-[orientation=vertical]:flex-col\",\n className,\n )}\n {...props}\n />\n );\n}\n\ntype ResizablePanelProps = Omit<\n React.ComponentProps<typeof Panel>,\n \"panelRef\"\n> & {\n /** Enable smooth CSS transition for collapse/expand. Drag resizing stays instant. @default false */\n animated?: boolean;\n /** Controlled collapsed state. When provided, the panel syncs to this value. */\n collapsed?: boolean;\n};\n\n/**\n * Individual resizable panel within a ResizablePanelGroup.\n * Supports size constraints (minSize, maxSize), default sizing, and collapse behavior.\n *\n * Control collapse state declaratively via the `collapsed` prop.\n * When `animated` is true, collapse/expand transitions are smooth.\n *\n * @example\n * <ResizablePanel collapsed={isCollapsed} animated collapsible>\n */\nfunction ResizablePanel({\n className,\n animated,\n collapsed,\n ...props\n}: ResizablePanelProps) {\n const panelRef = React.useRef<PanelImperativeHandle>(null);\n const elementRef = React.useRef<HTMLDivElement>(null);\n const animatedRef = React.useRef(animated);\n const isFirstRender = React.useRef(true);\n\n // Keep the ref in sync with prop changes\n React.useEffect(() => {\n animatedRef.current = animated;\n }, [animated]);\n\n /** Apply transition, call action, then remove transition after duration */\n const withTransition = React.useCallback(\n (action: () => void, skipAnimation = false) => {\n const panelEl = elementRef.current?.closest(\n '[data-panel][data-slot=\"resizable-panel\"]',\n ) as HTMLElement | null;\n\n if (animatedRef.current && panelEl && !skipAnimation) {\n panelEl.style.transition = TRANSITION_STYLE;\n action();\n setTimeout(() => {\n panelEl.style.transition = \"\";\n }, TRANSITION_DURATION);\n } else {\n action();\n }\n },\n [],\n );\n\n // Sync collapsed prop to panel state\n React.useEffect(() => {\n if (collapsed === undefined) return;\n\n // On first render, defer to allow panel registration\n if (isFirstRender.current) {\n isFirstRender.current = false;\n // Use requestAnimationFrame to ensure panel is registered\n requestAnimationFrame(() => {\n if (collapsed) {\n panelRef.current?.collapse();\n }\n });\n return;\n }\n\n // After first render, check current state before acting\n const isCurrentlyCollapsed = panelRef.current?.isCollapsed() ?? false;\n if (collapsed === isCurrentlyCollapsed) return;\n\n if (collapsed) {\n withTransition(() => panelRef.current?.collapse());\n } else {\n withTransition(() => panelRef.current?.expand());\n }\n }, [collapsed, withTransition]);\n\n return (\n <Panel\n data-slot=\"resizable-panel\"\n panelRef={panelRef}\n elementRef={elementRef}\n className={cn(className)}\n {...props}\n />\n );\n}\n\ntype ResizableHandleProps = React.ComponentProps<typeof PanelResizeHandle> & {\n /** Display a visible grip indicator on the resize handle. @default false */\n withHandle?: boolean;\n};\n\n/**\n * Resize handle between panels. Appears as a thin line with optional visible grip indicator.\n * Supports keyboard navigation and focus states.\n */\nfunction ResizableHandle({\n withHandle,\n className,\n ...props\n}: ResizableHandleProps) {\n return (\n <PanelResizeHandle\n data-slot=\"resizable-handle\"\n className={cn(\n \"group bg-border focus-visible:ring-primary/50 focus-visible:bg-primary/50 relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:ring-offset-background focus-visible:outline-hidden data-[resize-handle-state=hover]:bg-primary/50 data-[resize-handle-state=drag]:bg-primary/50 data-[resize-handle-state=active]:bg-primary/50 aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90\",\n className,\n )}\n {...props}\n >\n {withHandle && (\n <div className=\"bg-border group-focus-visible:bg-primary/50 group-data-[resize-handle-state=hover]:bg-primary/50 group-data-[resize-handle-state=drag]:bg-primary/50 group-data-[resize-handle-state=active]:bg-primary/50 h-6 w-1 rounded-lg z-10 flex shrink-0\" />\n )}\n </PanelResizeHandle>\n );\n}\n\nexport {\n ResizablePanelGroup,\n ResizablePanel,\n ResizableHandle,\n type ResizablePanelGroupProps,\n type ResizablePanelProps,\n type ResizableHandleProps,\n};\n"],"names":["PanelGroup","Panel","PanelResizeHandle"],"mappings":";;;;;AAaA;AAGA;AAUA;AAA6B;AAC3B;AAEF;AACE;AACE;AAACA;AAAA;AACW;AACC;AACT;AACA;AAAA;AAEE;AAAA;AAGV;AAsBA;AAAwB;AACtB;AACA;AACA;AAEF;AACE;AACA;AACA;AACA;AAGA;AACE;AAAsB;AAIxB;AAA6B;AAEzB;AAAoC;AAClC;AAGF;AACE;AACA;AACA;AACE;AAA2B;AACP;AAEtB;AAAA;AACF;AACF;AACA;AAIF;AACE;AAGA;AACE;AAEA;AACE;AACE;AAAkB;AACpB;AAEF;AAAA;AAIF;AACA;AAEA;AACE;AAAiD;AAEjD;AAA+C;AACjD;AAGF;AACE;AAACC;AAAA;AACW;AACV;AACA;AACuB;AACnB;AAAA;AAGV;AAWA;AAAyB;AACvB;AACA;AAEF;AACE;AACE;AAACC;AAAA;AACW;AACC;AACT;AACA;AAAA;AAEE;AAGgQ;AAAA;AAI1Q;;;;;;"}
1
+ {"version":3,"file":"resizable.js","sources":["../../../src/components/ui/resizable.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport {\n Group as PanelGroup,\n Panel,\n Separator as PanelResizeHandle,\n type PanelImperativeHandle,\n} from \"react-resizable-panels\";\n\nimport { cn } from \"@/lib/utils\";\n\n/** Default duration of the collapse/expand animation in milliseconds. */\nconst DEFAULT_DURATION = 400;\n\n/** Default easing function for collapse/expand animations. */\nconst DEFAULT_EASING = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Generate CSS transition value for animated collapse/expand. */\nfunction getTransitionStyle(durationMs: number, easing: string): string {\n const seconds = durationMs / 1000;\n return `flex-grow ${seconds}s ${easing}, flex-basis ${seconds}s ${easing}`;\n}\n\ntype ResizablePanelGroupProps = React.ComponentProps<typeof PanelGroup>;\n\n/**\n * Container for resizable panels. Manages layout orientation and panel resizing behavior.\n * Supports both horizontal and vertical layouts with optional persistence via `id` prop.\n */\nfunction ResizablePanelGroup({\n className,\n ...props\n}: ResizablePanelGroupProps) {\n return (\n <PanelGroup\n data-slot=\"resizable-panel-group\"\n className={cn(\n \"flex h-full w-full aria-[orientation=vertical]:flex-col\",\n className,\n )}\n {...props}\n />\n );\n}\n\ntype ResizablePanelProps = Omit<\n React.ComponentProps<typeof Panel>,\n \"panelRef\"\n> & {\n /** Enable smooth CSS transition for collapse/expand. Drag resizing stays instant. @default false */\n animated?: boolean;\n /** Controlled collapsed state. When provided, the panel syncs to this value. */\n collapsed?: boolean;\n /** Animation duration in milliseconds. Only applies when `animated` is true. @default 400 */\n duration?: number;\n};\n\n/**\n * Individual resizable panel within a ResizablePanelGroup.\n * Supports size constraints (minSize, maxSize), default sizing, and collapse behavior.\n *\n * Control collapse state declaratively via the `collapsed` prop.\n * When `animated` is true, collapse/expand transitions are smooth.\n *\n * @example\n * <ResizablePanel collapsed={isCollapsed} animated collapsible>\n */\nfunction ResizablePanel({\n className,\n animated,\n collapsed,\n duration = DEFAULT_DURATION,\n ...props\n}: ResizablePanelProps) {\n const panelRef = React.useRef<PanelImperativeHandle>(null);\n const elementRef = React.useRef<HTMLDivElement>(null);\n const animatedRef = React.useRef(animated);\n const durationRef = React.useRef(duration);\n const prevCollapsedRef = React.useRef<boolean | undefined>(undefined);\n\n // Keep the refs in sync with prop changes\n React.useEffect(() => {\n animatedRef.current = animated;\n durationRef.current = duration;\n }, [animated, duration]);\n\n /** Apply transition, call action, then remove transition after duration */\n const withTransition = React.useCallback(\n (action: () => void, skipAnimation = false) => {\n const panelEl = elementRef.current?.closest(\n '[data-panel][data-slot=\"resizable-panel\"]',\n ) as HTMLElement | null;\n\n if (animatedRef.current && panelEl && !skipAnimation) {\n const ms = durationRef.current;\n // Read easing from CSS variable, fallback to default\n const easing =\n getComputedStyle(panelEl)\n .getPropertyValue(\"--resizable-easing\")\n .trim() || DEFAULT_EASING;\n panelEl.style.transition = getTransitionStyle(ms, easing);\n action();\n setTimeout(() => {\n panelEl.style.transition = \"\";\n }, ms);\n } else {\n action();\n }\n },\n [],\n );\n\n // Sync collapsed prop to panel state\n React.useEffect(() => {\n if (collapsed === undefined) return;\n\n const isFirstSync = prevCollapsedRef.current === undefined;\n const hasChanged = collapsed !== prevCollapsedRef.current;\n prevCollapsedRef.current = collapsed;\n\n if (!hasChanged && !isFirstSync) return;\n\n // Helper to safely call collapse/expand with retry logic\n const syncState = () => {\n try {\n const isCurrentlyCollapsed = panelRef.current?.isCollapsed() ?? false;\n if (collapsed === isCurrentlyCollapsed) return;\n\n if (collapsed) {\n if (isFirstSync) {\n // First sync: no animation\n panelRef.current?.collapse();\n } else {\n withTransition(() => panelRef.current?.collapse());\n }\n } else {\n withTransition(() => panelRef.current?.expand());\n }\n } catch {\n // Panel not ready yet, retry after a tick\n setTimeout(syncState, 0);\n }\n };\n\n // Use setTimeout(0) to ensure we're after the panel group's layout effects\n setTimeout(syncState, 0);\n }, [collapsed, withTransition]);\n\n return (\n <Panel\n data-slot=\"resizable-panel\"\n panelRef={panelRef}\n elementRef={elementRef}\n className={cn(className)}\n {...props}\n />\n );\n}\n\ntype ResizableHandleProps = React.ComponentProps<typeof PanelResizeHandle> & {\n /** Display a visible grip indicator on the resize handle. @default false */\n withHandle?: boolean;\n};\n\n/**\n * Resize handle between panels. Appears as a thin line with optional visible grip indicator.\n * Supports keyboard navigation and focus states.\n */\nfunction ResizableHandle({\n withHandle,\n className,\n ...props\n}: ResizableHandleProps) {\n return (\n <PanelResizeHandle\n data-slot=\"resizable-handle\"\n className={cn(\n \"group bg-border focus-visible:ring-primary/50 focus-visible:bg-primary/50 relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:ring-offset-background focus-visible:outline-hidden data-[resize-handle-state=hover]:bg-primary/50 data-[resize-handle-state=drag]:bg-primary/50 data-[resize-handle-state=active]:bg-primary/50 aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90\",\n className,\n )}\n {...props}\n >\n {withHandle && (\n <div className=\"bg-border group-focus-visible:bg-primary/50 group-data-[resize-handle-state=hover]:bg-primary/50 group-data-[resize-handle-state=drag]:bg-primary/50 group-data-[resize-handle-state=active]:bg-primary/50 h-6 w-1 rounded-lg z-10 flex shrink-0\" />\n )}\n </PanelResizeHandle>\n );\n}\n\nexport {\n ResizablePanelGroup,\n ResizablePanel,\n ResizableHandle,\n type ResizablePanelGroupProps,\n type ResizablePanelProps,\n type ResizableHandleProps,\n};\n"],"names":["PanelGroup","Panel","PanelResizeHandle"],"mappings":";;;;;AAaA;AAGA;AAGA;AACE;AACA;AACF;AAQA;AAA6B;AAC3B;AAEF;AACE;AACE;AAACA;AAAA;AACW;AACC;AACT;AACA;AAAA;AAEE;AAAA;AAGV;AAwBA;AAAwB;AACtB;AACA;AACA;AACW;AAEb;AACE;AACA;AACA;AACA;AACA;AAGA;AACE;AACA;AAAsB;AAIxB;AAA6B;AAEzB;AAAoC;AAClC;AAGF;AACE;AAEA;AAIA;AACA;AACA;AACE;AAA2B;AACxB;AAEL;AAAA;AACF;AACF;AACA;AAIF;AACE;AAEA;AACA;AACA;AAEA;AAGA;AACE;AACE;AACA;AAEA;AACE;AAEE;AAAkB;AAElB;AAAiD;AACnD;AAEA;AAA+C;AACjD;AAGA;AAAuB;AACzB;AAIF;AAAuB;AAGzB;AACE;AAACC;AAAA;AACW;AACV;AACA;AACuB;AACnB;AAAA;AAGV;AAWA;AAAyB;AACvB;AACA;AAEF;AACE;AACE;AAACC;AAAA;AACW;AACC;AACT;AACA;AAAA;AAEE;AAGgQ;AAAA;AAI1Q;;;;;;"}
@@ -72,6 +72,7 @@ All panels inherit props from react-resizable-panels Panel component.
72
72
  | collapsedSize | number \| string | 0 | Size when collapsed |
73
73
  | collapsed | boolean | - | Controlled collapsed state. Panel syncs to this value. |
74
74
  | animated | boolean | false | Enable smooth CSS transition for collapse/expand. Drag resizing stays instant. |
75
+ | duration | number | 400 | Animation duration in milliseconds. Only applies when `animated` is true. |
75
76
  | onResize | (size: { asPercentage: number; inPixels: number }) => void | - | Called when panel is resized |
76
77
  | onCollapse | () => void | - | Called when panel collapses |
77
78
  | onExpand | () => void | - | Called when panel expands |
@@ -230,6 +231,22 @@ function MonitoredPanels() {
230
231
 
231
232
  Use `collapsed` prop for declarative control and `animated` for smooth transitions. Drag resizing remains instant (no animation lag).
232
233
 
234
+ **Customization:**
235
+ - `duration` prop controls speed (default 400ms)
236
+ - `--resizable-easing` CSS variable controls easing curve (default `cubic-bezier(0.16, 1, 0.3, 1)`)
237
+
238
+ ```tsx
239
+ // Custom duration
240
+ <ResizablePanel animated duration={200} collapsed={isCollapsed} />
241
+
242
+ // Custom easing via Tailwind arbitrary property
243
+ <ResizablePanel
244
+ className="[--resizable-easing:ease-out]"
245
+ animated
246
+ collapsed={isCollapsed}
247
+ />
248
+ ```
249
+
233
250
  ```tsx
234
251
  import { useState } from "react"
235
252
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neynar/ui",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "license": "MIT",
5
5
  "author": "Neynar Inc.",
6
6
  "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.",
@@ -10,13 +10,17 @@ import {
10
10
 
11
11
  import { cn } from "@/lib/utils";
12
12
 
13
- /** Duration of the collapse/expand animation in milliseconds. */
14
- const TRANSITION_DURATION = 300;
13
+ /** Default duration of the collapse/expand animation in milliseconds. */
14
+ const DEFAULT_DURATION = 400;
15
15
 
16
- /** CSS transition value for animated collapse/expand. */
17
- const TRANSITION_STYLE =
18
- "flex-grow 0.3s cubic-bezier(0.16, 1, 0.3, 1), flex-basis 0.3s cubic-bezier(0.16, 1, 0.3, 1)";
16
+ /** Default easing function for collapse/expand animations. */
17
+ const DEFAULT_EASING = "cubic-bezier(0.16, 1, 0.3, 1)";
19
18
 
19
+ /** Generate CSS transition value for animated collapse/expand. */
20
+ function getTransitionStyle(durationMs: number, easing: string): string {
21
+ const seconds = durationMs / 1000;
22
+ return `flex-grow ${seconds}s ${easing}, flex-basis ${seconds}s ${easing}`;
23
+ }
20
24
 
21
25
  type ResizablePanelGroupProps = React.ComponentProps<typeof PanelGroup>;
22
26
 
@@ -48,6 +52,8 @@ type ResizablePanelProps = Omit<
48
52
  animated?: boolean;
49
53
  /** Controlled collapsed state. When provided, the panel syncs to this value. */
50
54
  collapsed?: boolean;
55
+ /** Animation duration in milliseconds. Only applies when `animated` is true. @default 400 */
56
+ duration?: number;
51
57
  };
52
58
 
53
59
  /**
@@ -64,17 +70,20 @@ function ResizablePanel({
64
70
  className,
65
71
  animated,
66
72
  collapsed,
73
+ duration = DEFAULT_DURATION,
67
74
  ...props
68
75
  }: ResizablePanelProps) {
69
76
  const panelRef = React.useRef<PanelImperativeHandle>(null);
70
77
  const elementRef = React.useRef<HTMLDivElement>(null);
71
78
  const animatedRef = React.useRef(animated);
72
- const isFirstRender = React.useRef(true);
79
+ const durationRef = React.useRef(duration);
80
+ const prevCollapsedRef = React.useRef<boolean | undefined>(undefined);
73
81
 
74
- // Keep the ref in sync with prop changes
82
+ // Keep the refs in sync with prop changes
75
83
  React.useEffect(() => {
76
84
  animatedRef.current = animated;
77
- }, [animated]);
85
+ durationRef.current = duration;
86
+ }, [animated, duration]);
78
87
 
79
88
  /** Apply transition, call action, then remove transition after duration */
80
89
  const withTransition = React.useCallback(
@@ -84,11 +93,17 @@ function ResizablePanel({
84
93
  ) as HTMLElement | null;
85
94
 
86
95
  if (animatedRef.current && panelEl && !skipAnimation) {
87
- panelEl.style.transition = TRANSITION_STYLE;
96
+ const ms = durationRef.current;
97
+ // Read easing from CSS variable, fallback to default
98
+ const easing =
99
+ getComputedStyle(panelEl)
100
+ .getPropertyValue("--resizable-easing")
101
+ .trim() || DEFAULT_EASING;
102
+ panelEl.style.transition = getTransitionStyle(ms, easing);
88
103
  action();
89
104
  setTimeout(() => {
90
105
  panelEl.style.transition = "";
91
- }, TRANSITION_DURATION);
106
+ }, ms);
92
107
  } else {
93
108
  action();
94
109
  }
@@ -100,27 +115,36 @@ function ResizablePanel({
100
115
  React.useEffect(() => {
101
116
  if (collapsed === undefined) return;
102
117
 
103
- // On first render, defer to allow panel registration
104
- if (isFirstRender.current) {
105
- isFirstRender.current = false;
106
- // Use requestAnimationFrame to ensure panel is registered
107
- requestAnimationFrame(() => {
118
+ const isFirstSync = prevCollapsedRef.current === undefined;
119
+ const hasChanged = collapsed !== prevCollapsedRef.current;
120
+ prevCollapsedRef.current = collapsed;
121
+
122
+ if (!hasChanged && !isFirstSync) return;
123
+
124
+ // Helper to safely call collapse/expand with retry logic
125
+ const syncState = () => {
126
+ try {
127
+ const isCurrentlyCollapsed = panelRef.current?.isCollapsed() ?? false;
128
+ if (collapsed === isCurrentlyCollapsed) return;
129
+
108
130
  if (collapsed) {
109
- panelRef.current?.collapse();
131
+ if (isFirstSync) {
132
+ // First sync: no animation
133
+ panelRef.current?.collapse();
134
+ } else {
135
+ withTransition(() => panelRef.current?.collapse());
136
+ }
137
+ } else {
138
+ withTransition(() => panelRef.current?.expand());
110
139
  }
111
- });
112
- return;
113
- }
114
-
115
- // After first render, check current state before acting
116
- const isCurrentlyCollapsed = panelRef.current?.isCollapsed() ?? false;
117
- if (collapsed === isCurrentlyCollapsed) return;
118
-
119
- if (collapsed) {
120
- withTransition(() => panelRef.current?.collapse());
121
- } else {
122
- withTransition(() => panelRef.current?.expand());
123
- }
140
+ } catch {
141
+ // Panel not ready yet, retry after a tick
142
+ setTimeout(syncState, 0);
143
+ }
144
+ };
145
+
146
+ // Use setTimeout(0) to ensure we're after the panel group's layout effects
147
+ setTimeout(syncState, 0);
124
148
  }, [collapsed, withTransition]);
125
149
 
126
150
  return (
@@ -73,6 +73,10 @@
73
73
  body {
74
74
  @apply font-sans bg-background text-foreground;
75
75
  }
76
+ :root {
77
+ /* Resizable panel animation easing - override in theme or component */
78
+ --resizable-easing: cubic-bezier(0.16, 1, 0.3, 1);
79
+ }
76
80
  }
77
81
 
78
82
  /* Surface blur effect - value comes from theme's --surface-blur variable */