File "hover-dropdown.tsx"
Full Path: /home/trinadezambia/public_html/student_panel/src/components/store/slices/hover-dropdown.tsx
File size: 3.67 KB
MIME-type: text/x-java
Charset: utf-8
'use client';
import React, { useState, useRef, useEffect } from 'react';
import { cn } from '@/components/lib/utils';
interface HoverDropdownProps {
trigger: React.ReactNode;
children: React.ReactNode;
align?: 'start' | 'center' | 'end';
className?: string;
delay?: number; // Delay in milliseconds before showing dropdown
}
export function HoverDropdown({
trigger,
children,
align = 'end',
className,
delay = 150,
}: HoverDropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const [isVisible, setIsVisible] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
// Handle mouse enter with delay
const handleMouseEnter = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setIsOpen(true);
// Small delay to ensure smooth animation
setTimeout(() => setIsVisible(true), 10);
}, delay);
};
// Handle mouse leave
const handleMouseLeave = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
setIsVisible(false);
// Wait for animation to complete before hiding
setTimeout(() => setIsOpen(false), 150);
};
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
// Get alignment classes
const getAlignmentClasses = () => {
switch (align) {
case 'start':
return 'left-0';
case 'center':
return 'left-1/2 transform -translate-x-1/2';
case 'end':
return 'right-0';
default:
return 'right-0';
}
};
return (
<div
className="relative inline-block"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{/* Trigger element */}
<div className="cursor-pointer">{trigger}</div>
{/* Dropdown content */}
{isOpen && (
<div
ref={dropdownRef}
className={cn(
'absolute top-full z-50 mt-1 min-w-[8rem] overflow-hidden rounded-md border bg-white shadow-md',
'transition-all duration-200 ease-in-out',
isVisible
? 'opacity-100 scale-100 translate-y-0'
: 'opacity-0 scale-95 -translate-y-1',
getAlignmentClasses(),
className
)}
>
{children}
</div>
)}
</div>
);
}
// Dropdown item component
interface HoverDropdownItemProps {
children: React.ReactNode;
onClick?: () => void;
className?: string;
disabled?: boolean;
}
export function HoverDropdownItem({
children,
onClick,
className,
disabled = false,
}: HoverDropdownItemProps) {
return (
<div
className={cn(
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors',
'hover:bg-gray-100 focus:bg-gray-100',
disabled && 'opacity-50 cursor-not-allowed',
className
)}
onClick={disabled ? undefined : onClick}
>
{children}
</div>
);
}
// Dropdown separator component
export function HoverDropdownSeparator({ className }: { className?: string }) {
return <div className={cn("my-1 h-px bg-gray-200", className)} />;
}
// Dropdown label component
interface HoverDropdownLabelProps {
children: React.ReactNode;
className?: string;
}
export function HoverDropdownLabel({
children,
className,
}: HoverDropdownLabelProps) {
return (
<div
className={cn(
'px-2 py-1.5 text-sm font-semibold text-gray-900',
className
)}
>
{children}
</div>
);
}