"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 AccordionItem { title: string; description: string; } export interface BlurTextProps { text?: string; delay?: number; startDelay?: number; className?: string; animateBy?: "words" | "letters"; direction?: "top" | "bottom"; stiffness?: number; damping?: number; mass?: number; blurAmount?: number; onAnimationComplete?: () => void; } export const BlurText: React.FC = ({ text = "", delay, startDelay = 0.05, className = "", animateBy = "letters", direction = "bottom", stiffness = 180, damping = 22, mass = 0.6, blurAmount = 8, onAnimationComplete, }) => { const prefersReducedMotion = useReducedMotion(); const words = useMemo(() => text.split(" "), [text]); const totalChars = useMemo(() => text.length, [text]); const effectiveDelay = useMemo(() => { if (delay !== undefined) return delay; if (animateBy === "words") return 0.05; return Math.max(0.007, Math.min(0.016, 1.1 / Math.max(totalChars, 1))); }, [delay, animateBy, totalChars]); let charCounter = 0; if (prefersReducedMotion) { return

{text}

; } return (

{words.map((word, wordIndex) => { if (animateBy === "words") { const currentWordIndex = wordIndex; const isLast = currentWordIndex === words.length - 1; const wordDelay = startDelay + currentWordIndex * effectiveDelay; return ( {wordIndex < words.length - 1 && ( )} ); } const letters = word.split(""); return ( {wordIndex < words.length - 1 && ( )} ); })}

); }; export interface AccordionProps extends Omit< React.HTMLAttributes, "onToggle" > { items: AccordionItem[]; defaultIndex?: number | null; collapsible?: boolean; letterDelay?: number; stiffness?: number; damping?: number; mass?: number; blurAmount?: number; closeOnClickOutside?: boolean; onToggle?: (index: number | null) => void; } export const Accordion = forwardRef( ( { items, className, defaultIndex = 1, collapsible = true, letterDelay, stiffness = 180, damping = 22, mass = 0.6, blurAmount = 8, 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); }; return (
{items.map((item, index) => { const isOpen = activeIndex === index; const triggerId = `${baseId}-trigger-${index}`; const panelId = `${baseId}-panel-${index}`; return (
{isOpen && (
toggleAccordion(index)} >
)}
); })}
); }, ); Accordion.displayName = "Accordion"; export default Accordion;