({
storageKey,
minHeight,
maxHeight,
side,
panelRef,
}: UseResizablePanelHeightOptions)
| 223 | * ref so mousemove never triggers a React re-render — only mouseup commits. |
| 224 | */ |
| 225 | export function useResizablePanelHeight({ |
| 226 | storageKey, |
| 227 | minHeight, |
| 228 | maxHeight, |
| 229 | side, |
| 230 | panelRef, |
| 231 | }: UseResizablePanelHeightOptions): { |
| 232 | height: number | null; |
| 233 | handleMouseDown: PanelResizeMouseDown; |
| 234 | } { |
| 235 | const [height, setHeight] = useState<number | null>(() => { |
| 236 | const stored = localStorage.getItem(storageKey); |
| 237 | if (stored) { |
| 238 | const parsed = parseInt(stored, 10); |
| 239 | if (!isNaN(parsed) && parsed >= minHeight && parsed <= maxHeight) |
| 240 | return parsed; |
| 241 | } |
| 242 | return null; |
| 243 | }); |
| 244 | |
| 245 | const isDragging = useRef(false); |
| 246 | const startY = useRef(0); |
| 247 | const startHeight = useRef(0); |
| 248 | |
| 249 | const handleMouseDown = useCallback<PanelResizeMouseDown>( |
| 250 | (e, cursorOverride) => { |
| 251 | e.preventDefault(); |
| 252 | isDragging.current = true; |
| 253 | startY.current = e.clientY; |
| 254 | // Anchor the drag to whatever height the panel is ACTUALLY rendering — |
| 255 | // this matters most before first drag when `height` is still null. |
| 256 | const rendered = panelRef.current?.getBoundingClientRect().height ?? 0; |
| 257 | startHeight.current = height ?? Math.round(rendered); |
| 258 | document.body.style.cursor = cursorOverride ?? 'row-resize'; |
| 259 | document.body.style.userSelect = 'none'; |
| 260 | }, |
| 261 | [height, panelRef], |
| 262 | ); |
| 263 | |
| 264 | useEffect(() => { |
| 265 | let pending: number | null = null; |
| 266 | let rafId: number | null = null; |
| 267 | |
| 268 | const flush = () => { |
| 269 | rafId = null; |
| 270 | const panel = panelRef.current; |
| 271 | if (pending == null || !panel) return; |
| 272 | // Clamp pending to whatever the parent currently allows so the drag |
| 273 | // stops cleanly at the available limit instead of fighting the |
| 274 | // ResizeObserver below (which would otherwise observe-and-clamp on |
| 275 | // every frame, causing twitch). |
| 276 | const parent = panel.parentElement; |
| 277 | if (parent) { |
| 278 | const panelRect = panel.getBoundingClientRect(); |
| 279 | const parentRect = parent.getBoundingClientRect(); |
| 280 | const available = parentRect.bottom - panelRect.top - 4; |
| 281 | if (available > 0 && pending > available) pending = available; |
| 282 | } |
no test coverage detected