Files
zerobyte/app/client/components/ui/collapsible.tsx
2026-01-04 15:40:09 +01:00

99 lines
2.8 KiB
TypeScript

import * as React from "react";
import { ChevronDown } from "lucide-react";
import { cn } from "~/client/lib/utils";
interface CollapsibleProps extends React.HTMLAttributes<HTMLDivElement> {
open?: boolean;
onOpenChange?: (open: boolean) => void;
defaultOpen?: boolean;
}
const CollapsibleContext = React.createContext<{
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}>({
open: false,
setOpen: () => {},
});
const Collapsible = React.forwardRef<HTMLDivElement, CollapsibleProps>(
({ className, open: controlledOpen, onOpenChange, defaultOpen = false, children, ...props }, ref) => {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
const isControlled = controlledOpen !== undefined;
const open = isControlled ? controlledOpen : uncontrolledOpen;
const setOpen = React.useCallback(
(value: React.SetStateAction<boolean>) => {
const newValue = typeof value === "function" ? value(open) : value;
if (!isControlled) {
setUncontrolledOpen(newValue);
}
onOpenChange?.(newValue);
},
[isControlled, open, onOpenChange],
);
return (
<CollapsibleContext.Provider value={{ open, setOpen }}>
<div ref={ref} className={cn(className)} {...props}>
{children}
</div>
</CollapsibleContext.Provider>
);
},
);
Collapsible.displayName = "Collapsible";
interface CollapsibleTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
const CollapsibleTrigger = React.forwardRef<HTMLButtonElement, CollapsibleTriggerProps>(
({ className, children, ...props }, ref) => {
const { open, setOpen } = React.useContext(CollapsibleContext);
return (
<button
ref={ref}
type="button"
className={cn(
"flex gap-2 items-center justify-between py-2 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
data-state={open ? "open" : "closed"}
onClick={() => setOpen(!open)}
{...props}
>
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
{children}
</button>
);
},
);
CollapsibleTrigger.displayName = "CollapsibleTrigger";
interface CollapsibleContentProps extends React.HTMLAttributes<HTMLDivElement> {}
const CollapsibleContent = React.forwardRef<HTMLDivElement, CollapsibleContentProps>(
({ className, children, ...props }, ref) => {
const { open } = React.useContext(CollapsibleContext);
return (
<div
ref={ref}
className={cn(
"overflow-hidden transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
className,
)}
data-state={open ? "open" : "closed"}
hidden={!open}
{...props}
>
{open && children}
</div>
);
},
);
CollapsibleContent.displayName = "CollapsibleContent";
export { Collapsible, CollapsibleTrigger, CollapsibleContent };