"use client" import * as React from "react" import { cn } from "@/lib/utils" interface ScrollAreaProps extends React.HTMLAttributes { orientation?: "vertical" | "horizontal" | "both" } function ScrollArea({ className, children, orientation = "vertical", ...props }: ScrollAreaProps) { const viewportRef = React.useRef(null) const [showVerticalScrollbar, setShowVerticalScrollbar] = React.useState(false) const [showHorizontalScrollbar, setShowHorizontalScrollbar] = React.useState(false) const [scrollTop, setScrollTop] = React.useState(0) const [scrollLeft, setScrollLeft] = React.useState(0) const [viewportHeight, setViewportHeight] = React.useState(0) const [viewportWidth, setViewportWidth] = React.useState(0) const [contentHeight, setContentHeight] = React.useState(0) const [contentWidth, setContentWidth] = React.useState(0) React.useEffect(() => { const viewport = viewportRef.current if (!viewport) return const updateScrollInfo = () => { const hasVerticalScroll = viewport.scrollHeight > viewport.clientHeight const hasHorizontalScroll = viewport.scrollWidth > viewport.clientWidth setShowVerticalScrollbar( hasVerticalScroll && (orientation === "vertical" || orientation === "both") ) setShowHorizontalScrollbar( hasHorizontalScroll && (orientation === "horizontal" || orientation === "both") ) setViewportHeight(viewport.clientHeight) setViewportWidth(viewport.clientWidth) setContentHeight(viewport.scrollHeight) setContentWidth(viewport.scrollWidth) } const handleScroll = () => { setScrollTop(viewport.scrollTop) setScrollLeft(viewport.scrollLeft) } updateScrollInfo() viewport.addEventListener("scroll", handleScroll) const resizeObserver = new ResizeObserver(updateScrollInfo) resizeObserver.observe(viewport) // Also observe children for content changes const mutationObserver = new MutationObserver(updateScrollInfo) mutationObserver.observe(viewport, { childList: true, subtree: true }) return () => { viewport.removeEventListener("scroll", handleScroll) resizeObserver.disconnect() mutationObserver.disconnect() } }, [orientation]) return (
{children}
{showVerticalScrollbar && ( )} {showHorizontalScrollbar && ( )}
) } interface ScrollBarProps extends React.HTMLAttributes { orientation?: "vertical" | "horizontal" viewportRef?: React.RefObject scrollPosition?: number viewportSize?: number contentSize?: number } function ScrollBar({ className, orientation = "vertical", viewportRef, scrollPosition = 0, viewportSize = 0, contentSize = 0, ...props }: ScrollBarProps) { const [isDragging, setIsDragging] = React.useState(false) const [isHovered, setIsHovered] = React.useState(false) const scrollbarRef = React.useRef(null) const startPosRef = React.useRef(0) const startScrollRef = React.useRef(0) const thumbSize = contentSize > 0 ? Math.max((viewportSize / contentSize) * 100, 10) : 0 const thumbPosition = contentSize > viewportSize ? (scrollPosition / (contentSize - viewportSize)) * (100 - thumbSize) : 0 const handleThumbMouseDown = (e: React.MouseEvent) => { e.preventDefault() setIsDragging(true) startPosRef.current = orientation === "vertical" ? e.clientY : e.clientX startScrollRef.current = scrollPosition } React.useEffect(() => { if (!isDragging) return const handleMouseMove = (e: MouseEvent) => { const viewport = viewportRef?.current const scrollbar = scrollbarRef.current if (!viewport || !scrollbar) return const currentPos = orientation === "vertical" ? e.clientY : e.clientX const delta = currentPos - startPosRef.current const scrollbarSize = orientation === "vertical" ? scrollbar.clientHeight : scrollbar.clientWidth const scrollRatio = (contentSize - viewportSize) / (scrollbarSize * (1 - thumbSize / 100)) const newScrollPos = startScrollRef.current + delta * scrollRatio if (orientation === "vertical") { viewport.scrollTop = Math.max(0, Math.min(newScrollPos, contentSize - viewportSize)) } else { viewport.scrollLeft = Math.max(0, Math.min(newScrollPos, contentSize - viewportSize)) } } const handleMouseUp = () => { setIsDragging(false) } document.addEventListener("mousemove", handleMouseMove) document.addEventListener("mouseup", handleMouseUp) return () => { document.removeEventListener("mousemove", handleMouseMove) document.removeEventListener("mouseup", handleMouseUp) } }, [isDragging, orientation, viewportRef, contentSize, viewportSize, thumbSize]) const handleTrackClick = (e: React.MouseEvent) => { const viewport = viewportRef?.current const scrollbar = scrollbarRef.current if (!viewport || !scrollbar || e.target !== scrollbar) return const rect = scrollbar.getBoundingClientRect() const clickPos = orientation === "vertical" ? e.clientY - rect.top : e.clientX - rect.left const scrollbarSize = orientation === "vertical" ? rect.height : rect.width const clickRatio = clickPos / scrollbarSize const targetScroll = clickRatio * contentSize - viewportSize / 2 if (orientation === "vertical") { viewport.scrollTop = Math.max(0, Math.min(targetScroll, contentSize - viewportSize)) } else { viewport.scrollLeft = Math.max(0, Math.min(targetScroll, contentSize - viewportSize)) } } return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} onClick={handleTrackClick} {...props} >
) } export { ScrollArea, ScrollBar }