import { splitProps, createSignal, onMount, onCleanup, type Component, type JSX, } from 'solid-js'; import clsx from 'clsx'; interface CyberScrollbarProps { children: JSX.Element; class?: string; } /** * Custom scrollbar with glow edge and hexagonal crystal indicator. * Hides the native scrollbar and renders a custom overlay: * - Always-visible subtle glow from right edge * - Hexagonal crystal appears on mouse proximity to right edge (~40px) * - Crystal fills solid on click/drag for scroll control * - Crystal fades out when mouse leaves detection perimeter */ export const CyberScrollbar: Component = (rawProps) => { const [local, rest] = splitProps(rawProps, ['children', 'class']); let contentRef: HTMLDivElement | undefined; let wrapperRef: HTMLDivElement | undefined; const [scrollRatio, setScrollRatio] = createSignal(0); const [thumbHeight, setThumbHeight] = createSignal(0); const [crystalVisible, setCrystalVisible] = createSignal(false); const [crystalActive, setCrystalActive] = createSignal(false); const [crystalY, setCrystalY] = createSignal(0); const [needsScrollbar, setNeedsScrollbar] = createSignal(false); const DETECTION_PERIMETER = 40; // px from right edge const CRYSTAL_HEIGHT = 40; // px const recalculate = () => { if (!contentRef) return; const el = contentRef; const hasScroll = el.scrollHeight > el.clientHeight; setNeedsScrollbar(hasScroll); if (hasScroll) { const ratio = el.clientHeight / el.scrollHeight; setThumbHeight(Math.max(ratio * el.clientHeight, CRYSTAL_HEIGHT)); } }; const onScroll = () => { if (!contentRef) return; const el = contentRef; const maxScroll = el.scrollHeight - el.clientHeight; if (maxScroll > 0) { setScrollRatio(el.scrollTop / maxScroll); } }; const onWrapperMouseMove = (e: MouseEvent) => { if (!wrapperRef || !contentRef) return; const rect = wrapperRef.getBoundingClientRect(); const distFromRight = rect.right - e.clientX; if (distFromRight <= DETECTION_PERIMETER && needsScrollbar()) { setCrystalVisible(true); // Clamp crystal Y to within the scrollbar track const trackHeight = rect.height; const maxY = trackHeight - CRYSTAL_HEIGHT; const relativeY = e.clientY - rect.top - CRYSTAL_HEIGHT / 2; setCrystalY(Math.max(0, Math.min(relativeY, maxY))); } else { if (!crystalActive()) { setCrystalVisible(false); } } }; const onWrapperMouseLeave = () => { if (!crystalActive()) { setCrystalVisible(false); } }; const onCrystalMouseDown = (e: MouseEvent) => { e.preventDefault(); setCrystalActive(true); scrollToCrystalPosition(); const onMouseMove = (e: MouseEvent) => { if (!wrapperRef || !contentRef) return; const rect = wrapperRef.getBoundingClientRect(); const trackHeight = rect.height; const maxY = trackHeight - CRYSTAL_HEIGHT; const relativeY = e.clientY - rect.top - CRYSTAL_HEIGHT / 2; const clampedY = Math.max(0, Math.min(relativeY, maxY)); setCrystalY(clampedY); // Scroll content proportionally const ratio = clampedY / maxY; const maxScroll = contentRef.scrollHeight - contentRef.clientHeight; contentRef.scrollTop = ratio * maxScroll; setScrollRatio(ratio); }; const onMouseUp = () => { setCrystalActive(false); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }; const scrollToCrystalPosition = () => { if (!wrapperRef || !contentRef) return; const rect = wrapperRef.getBoundingClientRect(); const trackHeight = rect.height; const maxY = trackHeight - CRYSTAL_HEIGHT; const ratio = crystalY() / maxY; const maxScroll = contentRef.scrollHeight - contentRef.clientHeight; contentRef.scrollTop = ratio * maxScroll; setScrollRatio(ratio); }; let resizeObserver: ResizeObserver | undefined; onMount(() => { recalculate(); if (contentRef) { resizeObserver = new ResizeObserver(() => recalculate()); resizeObserver.observe(contentRef); } }); onCleanup(() => { resizeObserver?.disconnect(); }); // Compute the thumb position in the track const thumbTop = () => { if (!contentRef) return 0; const trackHeight = (wrapperRef?.clientHeight ?? 0) - CRYSTAL_HEIGHT; return scrollRatio() * trackHeight; }; return (
{/* Scrollable content area */}
{local.children}
{/* Glow edge — always visible when scrollable */}
{/* Track glow — shows scroll position */}
{/* Hexagonal crystal indicator */}
); };