"use client"; import React, { useState, useRef, useEffect, useMemo, forwardRef, useId, useImperativeHandle, } from "react"; import { motion, AnimatePresence, useReducedMotion } from "motion/react"; import { cn } from "@/lib/utils"; export interface DottedAccordionItem { title: string; description: string; } export interface DottedAccordionProps extends Omit< React.HTMLAttributes, "onToggle" > { items: DottedAccordionItem[]; defaultIndex?: number | null; collapsible?: boolean; extensionLength?: number; closeOnClickOutside?: boolean; onToggle?: (index: number | null) => void; } export const DottedAccordion = forwardRef( ( { items, className, defaultIndex = 0, collapsible = true, extensionLength = 48, closeOnClickOutside = true, onToggle, ...props }, ref, ) => { const [activeIndex, setActiveIndex] = useState(defaultIndex); const innerRef = useRef(null); const prefersReducedMotion = useReducedMotion(); const baseId = useId(); useImperativeHandle(ref, () => innerRef.current as HTMLDivElement); useEffect(() => { if (!closeOnClickOutside) return; const handleClickOutside = (event: MouseEvent) => { if ( innerRef.current && !innerRef.current.contains(event.target as Node) ) { setActiveIndex(null); onToggle?.(null); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [closeOnClickOutside, onToggle]); const toggleAccordion = (index: number) => { const nextIndex = activeIndex === index ? (collapsible ? null : index) : index; setActiveIndex(nextIndex); onToggle?.(nextIndex); }; const horizontalMaskStyle = useMemo( () => ({ WebkitMaskImage: `linear-gradient(to right, transparent, black ${extensionLength}px, black calc(100% - ${extensionLength}px), transparent)`, maskImage: `linear-gradient(to right, transparent, black ${extensionLength}px, black calc(100% - ${extensionLength}px), transparent)`, }), [extensionLength], ); const verticalMaskStyle = useMemo( () => ({ WebkitMaskImage: `linear-gradient(to bottom, transparent, black ${extensionLength}px, black calc(100% - ${extensionLength}px), transparent)`, maskImage: `linear-gradient(to bottom, transparent, black ${extensionLength}px, black calc(100% - ${extensionLength}px), transparent)`, }), [extensionLength], ); return (
{items.map((item, index) => { const isOpen = activeIndex === index; const triggerId = `${baseId}-trigger-${index}`; const panelId = `${baseId}-panel-${index}`; return (
{isOpen && ( <> )} {isOpen && (
toggleAccordion(index)} > {item.description}
)}
); })}
); }, ); DottedAccordion.displayName = "DottedAccordion"; export default DottedAccordion;