Files
vibn-frontend/components/sidebar/resizable-sidebar.tsx

144 lines
3.9 KiB
TypeScript

"use client";
import { useState, useCallback, useEffect, useRef, type ReactNode } from "react";
import { cn } from "@/lib/utils";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button";
interface ResizableSidebarProps {
children: ReactNode;
defaultWidth?: number;
minWidth?: number;
maxWidth?: number;
}
export function ResizableSidebar({
children,
defaultWidth = 250,
minWidth = 200,
maxWidth = 400,
}: ResizableSidebarProps) {
const [width, setWidth] = useState(defaultWidth);
const [isCollapsed, setIsCollapsed] = useState(false);
const [isResizing, setIsResizing] = useState(false);
const [showPeek, setShowPeek] = useState(false);
const sidebarRef = useRef<HTMLDivElement>(null);
const initialXRef = useRef<number>(0);
const initialWidthRef = useRef<number>(defaultWidth);
const startResizing = useCallback((e: React.MouseEvent) => {
setIsResizing(true);
initialXRef.current = e.clientX;
initialWidthRef.current = width;
e.preventDefault();
}, [width]);
const stopResizing = useCallback(() => {
setIsResizing(false);
}, []);
const resize = useCallback(
(e: MouseEvent) => {
if (isResizing) {
const deltaX = e.clientX - initialXRef.current;
const newWidth = initialWidthRef.current + deltaX;
const clampedWidth = Math.min(Math.max(newWidth, minWidth), maxWidth);
setWidth(clampedWidth);
}
},
[isResizing, minWidth, maxWidth]
);
useEffect(() => {
if (isResizing) {
window.addEventListener("mousemove", resize);
window.addEventListener("mouseup", stopResizing);
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
}
return () => {
window.removeEventListener("mousemove", resize);
window.removeEventListener("mouseup", stopResizing);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
}, [isResizing, resize, stopResizing]);
const handleMouseEnter = () => {
if (isCollapsed) {
setShowPeek(true);
}
};
const handleMouseLeave = () => {
if (isCollapsed) {
setTimeout(() => setShowPeek(false), 300);
}
};
return (
<>
{/* Peek trigger when collapsed */}
{isCollapsed && (
<div
className="absolute left-0 top-0 z-40 h-full w-2"
onMouseEnter={handleMouseEnter}
/>
)}
<aside
ref={sidebarRef}
style={{ width: isCollapsed ? 0 : `${width}px` }}
className={cn(
"relative flex flex-col border-r bg-card transition-all duration-300",
isCollapsed && "w-0 overflow-hidden"
)}
>
{!isCollapsed && children}
{/* Resize Handle */}
{!isCollapsed && (
<div
onMouseDown={startResizing}
className={cn(
"absolute right-0 top-0 h-full w-1 cursor-col-resize transition-colors hover:bg-primary/20",
isResizing && "bg-primary/40"
)}
/>
)}
{/* Collapse/Expand Button */}
<Button
variant="outline"
size="icon"
onClick={() => setIsCollapsed(!isCollapsed)}
className={cn(
"absolute -right-3 top-4 z-10 h-6 w-6 rounded-full shadow-sm",
isCollapsed && "left-2"
)}
>
{isCollapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</Button>
</aside>
{/* Peek Sidebar (when collapsed) */}
{isCollapsed && showPeek && (
<div
className="absolute left-0 top-0 z-30 h-full w-64 border-r bg-card shadow-lg"
style={{ width: `${defaultWidth}px` }}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{children}
</div>
)}
</>
);
}