mirror of
https://github.com/nicotsx/zerobyte.git
synced 2026-04-20 06:47:31 -04:00
99 lines
2.8 KiB
TypeScript
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 };
|