Files
2025-11-23 08:02:54 +01:00

203 lines
5.0 KiB
TypeScript

"use client";
import * as React from "react";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
interface AccordionContextValue {
value: string[];
onValueChange: (value: string[]) => void;
}
const AccordionContext = React.createContext<AccordionContextValue | undefined>(
undefined
);
interface AccordionProps {
type?: "single" | "multiple";
defaultValue?: string | string[];
value?: string | string[];
onValueChange?: (value: string | string[]) => void;
children: React.ReactNode;
className?: string;
}
const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
({ type = "single", defaultValue, value, onValueChange, children, className }, ref) => {
const [internalValue, setInternalValue] = React.useState<string[]>(
defaultValue
? Array.isArray(defaultValue)
? defaultValue
: [defaultValue]
: []
);
const controlledValue = value
? Array.isArray(value)
? value
: [value]
: undefined;
const currentValue = controlledValue ?? internalValue;
const handleValueChange = React.useCallback(
(newValue: string[]) => {
if (!controlledValue) {
setInternalValue(newValue);
}
if (onValueChange) {
onValueChange(type === "single" ? newValue[0] || "" : newValue);
}
},
[controlledValue, onValueChange, type]
);
const contextValue = React.useMemo(
() => ({
value: currentValue,
onValueChange: handleValueChange,
}),
[currentValue, handleValueChange]
);
return (
<AccordionContext.Provider value={contextValue}>
<div ref={ref} className={cn("space-y-2", className)}>
{children}
</div>
</AccordionContext.Provider>
);
}
);
Accordion.displayName = "Accordion";
interface AccordionItemProps {
value: string;
children: React.ReactNode;
className?: string;
}
const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(
({ value, children, className }, ref) => {
return (
<div
ref={ref}
className={cn("rounded-xl border border-gray-200 overflow-hidden", className)}
data-value={value}
>
{children}
</div>
);
}
);
AccordionItem.displayName = "AccordionItem";
interface AccordionTriggerProps {
children: React.ReactNode;
className?: string;
}
const AccordionTrigger = React.forwardRef<
HTMLButtonElement,
AccordionTriggerProps
>(({ children, className }, ref) => {
const context = React.useContext(AccordionContext);
if (!context) {
throw new Error("AccordionTrigger must be used within Accordion");
}
const item = React.useContext(ItemContext);
if (!item) {
throw new Error("AccordionTrigger must be used within AccordionItem");
}
const isOpen = context.value.includes(item.value);
const handleClick = () => {
const newValue = isOpen
? context.value.filter((v) => v !== item.value)
: [...context.value, item.value];
context.onValueChange(newValue);
};
return (
<button
ref={ref}
type="button"
onClick={handleClick}
className={cn(
"flex w-full items-center justify-between p-4 text-left font-medium text-primary transition-all hover:bg-secondary [&[data-state=open]>svg]:rotate-180",
className
)}
data-state={isOpen ? "open" : "closed"}
>
{children}
<ChevronDown className="h-5 w-5 shrink-0 transition-transform duration-200" />
</button>
);
});
AccordionTrigger.displayName = "AccordionTrigger";
interface AccordionContentProps {
children: React.ReactNode;
className?: string;
}
const ItemContext = React.createContext<{ value: string } | undefined>(
undefined
);
const AccordionContent = React.forwardRef<
HTMLDivElement,
AccordionContentProps
>(({ children, className }, ref) => {
const context = React.useContext(AccordionContext);
if (!context) {
throw new Error("AccordionContent must be used within Accordion");
}
const item = React.useContext(ItemContext);
if (!item) {
throw new Error("AccordionContent must be used within AccordionItem");
}
const isOpen = context.value.includes(item.value);
return (
<div
ref={ref}
className={cn(
"overflow-hidden transition-all",
isOpen ? "max-h-[1000px] opacity-100" : "max-h-0 opacity-0"
)}
>
<div className={cn("p-4 pt-0 text-gray-700", className)}>
{children}
</div>
</div>
);
});
AccordionContent.displayName = "AccordionContent";
const AccordionItemWithContext = React.forwardRef<
HTMLDivElement,
AccordionItemProps
>(({ value, children, ...props }, ref) => {
return (
<ItemContext.Provider value={{ value }}>
<AccordionItem ref={ref} value={value} {...props}>
{children}
</AccordionItem>
</ItemContext.Provider>
);
});
AccordionItemWithContext.displayName = "AccordionItem";
export {
Accordion,
AccordionItemWithContext as AccordionItem,
AccordionTrigger,
AccordionContent,
};