bookwiz.io / components / ResizablePanel.tsx
ResizablePanel.tsx
Raw
'use client'

import React, { useState, useRef, useEffect, useCallback, ReactNode } from 'react'
import { IoChevronBackOutline, IoChevronForwardOutline } from 'react-icons/io5'

interface ResizablePanelProps {
  children: ReactNode
  side: 'left' | 'right'
  minWidth?: number
  maxWidth?: number
  defaultWidth?: number
  defaultCollapsed?: boolean
  collapsible?: boolean
  persistKey?: string // For localStorage persistence
  className?: string
  onWidthChange?: (width: number) => void
  onCollapseChange?: (collapsed: boolean) => void
}

export default function ResizablePanel({
  children,
  side,
  minWidth = 200,
  maxWidth = 600,
  defaultWidth = 280,
  defaultCollapsed = false,
  collapsible = true,
  persistKey,
  className = '',
  onWidthChange,
  onCollapseChange
}: ResizablePanelProps) {
  const panelRef = useRef<HTMLDivElement>(null)
  const isDragging = useRef(false)
  const startX = useRef(0)
  const startWidth = useRef(0)

  // Load persisted state
  const loadPersistedState = useCallback(() => {
    if (!persistKey || typeof window === 'undefined') {
      return { width: defaultWidth, collapsed: defaultCollapsed }
    }

    try {
      const stored = localStorage.getItem(`panel_${persistKey}`)
      if (stored) {
        const parsed = JSON.parse(stored)
        return {
          width: Math.max(minWidth, Math.min(maxWidth, parsed.width || defaultWidth)),
          collapsed: parsed.collapsed ?? defaultCollapsed
        }
      }
    } catch (error) {
      console.warn('Failed to load panel state:', error)
    }

    return { width: defaultWidth, collapsed: defaultCollapsed }
  }, [persistKey, defaultWidth, defaultCollapsed, minWidth, maxWidth])

  const [panelState, setPanelState] = useState(loadPersistedState)

  // Save state to localStorage
  const saveState = useCallback((width: number, collapsed: boolean) => {
    if (!persistKey || typeof window === 'undefined') return

    try {
      localStorage.setItem(`panel_${persistKey}`, JSON.stringify({ width, collapsed }))
    } catch (error) {
      console.warn('Failed to save panel state:', error)
    }
  }, [persistKey])

  // Handle mouse move for resizing
  const handleMouseMove = useCallback((e: MouseEvent) => {
    if (!isDragging.current) return

    const deltaX = side === 'left' ? e.clientX - startX.current : startX.current - e.clientX
    const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth.current + deltaX))
    
    setPanelState(prev => {
      const updated = { ...prev, width: newWidth }
      saveState(updated.width, updated.collapsed)
      onWidthChange?.(newWidth)
      return updated
    })
  }, [side, minWidth, maxWidth, saveState, onWidthChange])

  // Handle mouse up to stop resizing
  const handleMouseUp = useCallback(() => {
    isDragging.current = false
    document.body.style.cursor = ''
    document.body.style.userSelect = ''
  }, [])

  // Set up global mouse events
  useEffect(() => {
    document.addEventListener('mousemove', handleMouseMove)
    document.addEventListener('mouseup', handleMouseUp)

    return () => {
      document.removeEventListener('mousemove', handleMouseMove)
      document.removeEventListener('mouseup', handleMouseUp)
    }
  }, [handleMouseMove, handleMouseUp])

  // Handle mouse down on resize handle
  const handleMouseDown = useCallback((e: React.MouseEvent) => {
    e.preventDefault()
    isDragging.current = true
    startX.current = e.clientX
    startWidth.current = panelState.width
    document.body.style.cursor = 'col-resize'
    document.body.style.userSelect = 'none'
  }, [panelState.width])

  // Toggle collapsed state
  const toggleCollapsed = useCallback(() => {
    setPanelState(prev => {
      const updated = { ...prev, collapsed: !prev.collapsed }
      saveState(updated.width, updated.collapsed)
      onCollapseChange?.(updated.collapsed)
      return updated
    })
  }, [saveState, onCollapseChange])

  // Determine panel styles
  const panelWidth = panelState.collapsed ? 0 : panelState.width
  const isLeft = side === 'left'

  const currentWidth = panelState.collapsed ? 24 : panelState.width

  return (
    <div
      ref={panelRef}
      className={`relative flex-shrink-0 ${className}`}
      style={{ 
        width: `${currentWidth}px`,
        transition: isDragging.current ? 'none' : 'width 300ms ease-in-out'
      }}
    >
      {/* Panel Content */}
      <div
        className={`h-full absolute inset-0 transition-opacity duration-300 ease-in-out ${
          panelState.collapsed ? 'opacity-0 pointer-events-none' : 'opacity-100'
        }`}
      >
        {children}
      </div>

      {/* Resize Handle */}
      {!panelState.collapsed && (
        <div
          className={`absolute top-0 bottom-0 w-1 bg-transparent hover:bg-slate-600/50 cursor-col-resize transition-colors duration-200 z-10 ${
            isLeft ? 'right-0' : 'left-0'
          }`}
          onMouseDown={handleMouseDown}
        >
          {/* Visual indicator for resize handle */}
          <div className={`absolute top-1/2 transform -translate-y-1/2 w-1 h-8 bg-slate-500/30 rounded-full transition-opacity duration-200 hover:opacity-100 opacity-0 ${
            isLeft ? 'right-0' : 'left-0'
          }`} />
        </div>
      )}

      {/* Collapse Button (when panel is open) */}
      {collapsible && !panelState.collapsed && (
        <button
          onClick={toggleCollapsed}
          className={`absolute top-1/2 transform -translate-y-1/2 w-6 h-6 bg-slate-800/80 hover:bg-slate-700/80 border border-slate-600/50 rounded-full flex items-center justify-center text-slate-300 hover:text-white transition-all duration-200 z-20 ${
            isLeft ? 'right-1' : 'left-1'
          }`}
          title="Collapse panel"
        >
          {isLeft ? <IoChevronBackOutline className="w-3 h-3" /> : <IoChevronForwardOutline className="w-3 h-3" />}
        </button>
      )}

      {/* Expand Button (when panel is collapsed) */}
      {collapsible && panelState.collapsed && (
        <button
          onClick={toggleCollapsed}
          className={`absolute top-1/2 transform -translate-y-1/2 w-6 h-6 bg-slate-800/90 hover:bg-slate-700/90 border border-slate-600/50 rounded-full flex items-center justify-center text-slate-300 hover:text-white transition-all duration-200 z-30 shadow-lg ${
            isLeft ? 'left-0' : 'right-0'
          }`}
          title="Expand panel"
        >
          {isLeft ? <IoChevronForwardOutline className="w-3 h-3" /> : <IoChevronBackOutline className="w-3 h-3" />}
        </button>
      )}
    </div>
  )
}