"use client"; import { CheckIcon, ChevronDownIcon } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Menu, MenuGroup, MenuGroupLabel, MenuItem, MenuPopup, MenuSeparator, MenuTrigger } from "@/components/ui/menu"; import { Spinner } from "@/components/ui/spinner"; import { cn } from "@/lib/utils"; export interface SplitAction { id: string; label: string; description?: string; icon?: ReactNode; done?: string; } export interface SplitButtonProps { actions: SplitAction[]; defaultActionId?: string; onAction?: (action: SplitAction) => void | Promise; remember?: boolean; menuLabel?: string; variant?: "default" | "outline"; disabled?: boolean; className?: string; } type Phase = "idle" | "running" | "done"; export function SplitButton({ actions, defaultActionId, onAction, remember = true, menuLabel = "Choose an action", variant = "default", disabled, className, }: SplitButtonProps) { const reduce = useReducedMotion(); const [primaryId, setPrimaryId] = useState(defaultActionId ?? actions[0]?.id); const [phase, setPhase] = useState("idle"); const [width, setWidth] = useState("auto"); const measureRef = useRef(null); const doneTimer = useRef(undefined); const mounted = useRef(true); useEffect(() => { mounted.current = true; return () => { mounted.current = false; clearTimeout(doneTimer.current); }; }, []); const primary = actions.find((a) => a.id === primaryId) ?? actions[0]; const label = phase === "done" ? (primary.done ?? primary.label) : primary.label; useLayoutEffect(() => { if (measureRef.current) setWidth(measureRef.current.offsetWidth); }, [label]); const run = async (action: SplitAction) => { if (phase === "running") return; if (remember) setPrimaryId(action.id); clearTimeout(doneTimer.current); setPhase("running"); try { await onAction?.(action); if (!mounted.current) return; setPhase("done"); doneTimer.current = window.setTimeout(() => setPhase("idle"), 1600); } catch { if (mounted.current) setPhase("idle"); } }; const ease = [0.23, 1, 0.32, 1] as const; const swap = { initial: reduce ? { opacity: 0 } : { opacity: 0, y: 6, filter: "blur(2px)" }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: reduce ? { opacity: 0 } : { opacity: 0, y: -6, filter: "blur(2px)" }, transition: { duration: 0.18, ease }, }; const icon = phase === "running" ? : phase === "done" ? : primary.icon; return (
} > {menuLabel} {actions.map((a) => ( run(a)}> {a.icon && ( {a.icon} )} {a.label} {a.description && {a.description}} ))} {remember && ( <>

The one you pick becomes the default.

)}
); }