"use client" import * as React from "react" import { GripVerticalIcon } from "lucide-react" import { cn } from "@/lib/utils" export type ResizablePanelGroupProps = React.ComponentProps<"div"> & { direction?: "horizontal" | "vertical" } export type ResizablePanelProps = React.ComponentProps<"div"> & { defaultSize?: number | string minSize?: number } type ResizablePanelInternalProps = ResizablePanelProps & { "data-panel-index"?: number } type ResizableHandleProps = React.ComponentProps<"button"> & { "data-handle-index"?: number } type ResizablePanelContextValue = { direction: "horizontal" | "vertical" getSize: (index: number) => number resizePanels: (index: number, deltaPx: number) => void } const ResizablePanelContext = React.createContext(null) function normalizeSizeValue(value: number | string | undefined) { if (typeof value === "number") return value if (typeof value === "string") { const parsed = Number.parseFloat(value.replace("%", "")) return Number.isFinite(parsed) ? parsed : undefined } return undefined } function ResizablePanelGroup({ direction = "horizontal", className, children, ...props }: ResizablePanelGroupProps) { const groupRef = React.useRef(null) const panelNodes = React.Children.toArray(children).filter((child) => React.isValidElement(child) && (child.type === ResizablePanel || (child as React.ReactElement<{ "data-slot"?: string }>).props["data-slot"] === "resizable-panel" || (child.type && (child.type as { name?: string }).name === "ResizablePanel"))) const panelCount = panelNodes.length const initialSizes = React.useMemo(() => { const declaredSizes = panelNodes.map((child) => normalizeSizeValue((child as React.ReactElement).props.defaultSize)) const declaredTotal = declaredSizes.reduce((sum, value) => sum + (value ?? 0), 0) const unresolvedCount = declaredSizes.filter((value) => value === undefined).length const remaining = Math.max(100 - declaredTotal, 0) const fallbackSize = unresolvedCount > 0 ? remaining / unresolvedCount : panelCount > 0 ? 100 / panelCount : 100 return declaredSizes.map((value) => value ?? fallbackSize) }, [panelCount, panelNodes]) const minSizes = React.useMemo( () => panelNodes.map((child) => (child as React.ReactElement).props.minSize ?? 15), [panelNodes] ) const [sizes, setSizes] = React.useState(initialSizes) const prevInitialSizes = React.useRef(initialSizes) React.useEffect(() => { const isSame = prevInitialSizes.current.length === initialSizes.length && prevInitialSizes.current.every((v, i) => v === initialSizes[i]) if (!isSame) { setSizes(initialSizes) prevInitialSizes.current = initialSizes } }, [initialSizes]) const resizePanels = React.useCallback( (index: number, deltaPx: number) => { const node = groupRef.current if (!node || index < 0 || index >= panelCount - 1) return const basis = direction === "horizontal" ? node.getBoundingClientRect().width : node.getBoundingClientRect().height if (!basis) return const deltaPercent = (deltaPx / basis) * 100 setSizes((currentSizes) => { const nextSizes = [...currentSizes] const previousSize = nextSizes[index] ?? 0 const followingSize = nextSizes[index + 1] ?? 0 const minPrevious = minSizes[index] ?? 15 const minFollowing = minSizes[index + 1] ?? 15 const requestedPrevious = previousSize + deltaPercent const clampedPrevious = Math.min(Math.max(requestedPrevious, minPrevious), previousSize + followingSize - minFollowing) const consumedDelta = clampedPrevious - previousSize nextSizes[index] = clampedPrevious nextSizes[index + 1] = followingSize - consumedDelta return nextSizes }) }, [direction, minSizes, panelCount] ) let panelIndexCounter = 0 let handleIndexCounter = 0 const processedChildren = React.Children.map(children, (child) => { if (!React.isValidElement(child)) return child if (child.type === ResizablePanel || (child as React.ReactElement<{ "data-slot"?: string }>).props["data-slot"] === "resizable-panel" || (child.type && (child.type as { name?: string }).name === "ResizablePanel")) { return React.cloneElement(child as React.ReactElement, { "data-panel-index": panelIndexCounter++ }) } if (child.type === ResizableHandle || (child as React.ReactElement<{ "data-slot"?: string }>).props["data-slot"] === "resizable-handle" || (child.type && (child.type as { name?: string }).name === "ResizableHandle")) { return React.cloneElement(child as React.ReactElement, { "data-handle-index": handleIndexCounter++ }) } return child }) return ( sizes[index] ?? 100 / Math.max(panelCount, 1), resizePanels, }} >
{processedChildren}
) } function ResizablePanel({ className, style, defaultSize, minSize: _minSize, ...props }: ResizablePanelInternalProps) { const context = React.useContext(ResizablePanelContext) const panelIndex = Number(props["data-panel-index"] ?? 0) const basis = context ? context.getSize(panelIndex) : normalizeSizeValue(defaultSize) ?? 100 return (
) } function ResizableHandle({ className, ...props }: ResizableHandleProps) { const context = React.useContext(ResizablePanelContext) const handleIndex = Number(props["data-handle-index"] ?? 0) const pointerStateRef = React.useRef<{ pointerId: number; axis: number } | null>(null) return ( ) } export { ResizableHandle, ResizablePanel, ResizablePanelGroup }