"use client";import * as React from "react";import { ChevronDown } from "lucide-react";import { EASE_OUT, EASE_SETTLE, INTERACTION_FOCUS } from "./ui-interaction-motion";export interface SplitMenuItem { id: string; label: string; icon?: React.ReactNode; onSelect: () => void; destructive?: boolean; disabled?: boolean;}export interface SplitButtonProps { label: string; icon?: React.ReactNode; onClick?: () => void; menuItems: SplitMenuItem[]; variant?: "primary" | "secondary" | "destructive"; size?: "sm" | "md" | "lg"; disabled?: boolean; loading?: boolean; menuLabel?: string; className?: string;}const variantStyles = { primary: { container: "bg-primary text-primary-foreground shadow-[0_1px_2px_rgba(48,93,222,0.18)] border-transparent", action: "hover:bg-primary/90 active:opacity-90 text-primary-foreground", trigger: "hover:bg-primary/90 active:opacity-90 text-primary-foreground", divider: "bg-primary-foreground/25", }, secondary: { container: "bg-card text-foreground shadow-[inset_0_0_0_1px_var(--border)] border-transparent", action: "hover:bg-accent active:bg-accent/80 text-foreground", trigger: "hover:bg-accent active:bg-accent/80 text-foreground", divider: "bg-border", }, destructive: { container: "bg-red-500/10 text-red-600 shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--color-red-500)_30%,transparent)] dark:text-red-400 border-transparent", action: "hover:bg-red-500/15 active:bg-red-500/20 text-red-600 dark:text-red-400", trigger: "hover:bg-red-500/15 active:bg-red-500/20 text-red-600 dark:text-red-400", divider: "bg-red-500/25", },};const sizeStyles = { sm: { height: "h-7 text-[12px] rounded-full", actionPad: "pl-3 pr-2 gap-1.5", triggerPad: "px-2", iconSize: 13, }, md: { height: "h-9 text-[13px] rounded-full", actionPad: "pl-4 pr-2.5 gap-2", triggerPad: "px-2.5", iconSize: 14, }, lg: { height: "h-10 text-[14px] rounded-full", actionPad: "pl-5 pr-3 gap-2.5", triggerPad: "px-3", iconSize: 16, },};export function SplitButton({ label, icon, onClick, menuItems, variant = "primary", size = "md", disabled = false, loading = false, menuLabel = "More actions", className = "",}: SplitButtonProps) { const [open, setOpen] = React.useState(false); const [pressedAction, setPressedAction] = React.useState(false); const containerRef = React.useRef<HTMLDivElement | null>(null); const menuRef = React.useRef<HTMLDivElement | null>(null); const triggerRef = React.useRef<HTMLButtonElement | null>(null); const v = variantStyles[variant]; const s = sizeStyles[size]; const inert = disabled || loading; // Close on outside click or Escape React.useEffect(() => { if (!open) return; const handlePointerDown = (e: PointerEvent) => { if ( containerRef.current && !containerRef.current.contains(e.target as Node) && menuRef.current && !menuRef.current.contains(e.target as Node) ) { setOpen(false); } }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { setOpen(false); triggerRef.current?.focus(); } }; document.addEventListener("pointerdown", handlePointerDown); document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("pointerdown", handlePointerDown); document.removeEventListener("keydown", handleKeyDown); }; }, [open]); // Keyboard navigation for dropdown menu const handleMenuKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => { const items = Array.from(menuRef.current?.querySelectorAll<HTMLButtonElement>("[data-menu-item]") ?? []); const currentIndex = items.findIndex((item) => item === document.activeElement); if (e.key === "ArrowDown") { e.preventDefault(); const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0; items[nextIndex]?.focus(); } else if (e.key === "ArrowUp") { e.preventDefault(); const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1; items[prevIndex]?.focus(); } else if (e.key === "Home") { e.preventDefault(); items[0]?.focus(); } else if (e.key === "End") { e.preventDefault(); items[items.length - 1]?.focus(); } }; const handleActionClick = (e: React.MouseEvent<HTMLButtonElement>) => { if (inert) { e.preventDefault(); return; } onClick?.(); }; const toggleMenu = (e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); if (inert) return; setOpen((prev) => !prev); }; return ( <div ref={containerRef} className={`relative inline-flex items-stretch select-none transition-colors isolate ${s.height} ${v.container} ${ disabled ? "opacity-45 cursor-not-allowed" : "" } ${className}`} > {/* Primary Action Button */} <button type="button" disabled={inert} onClick={handleActionClick} onPointerDown={() => !inert && setPressedAction(true)} onPointerUp={() => setPressedAction(false)} onPointerLeave={() => setPressedAction(false)} style={{ transform: pressedAction ? "scale(0.98)" : undefined, transition: `transform 160ms ${EASE_SETTLE}, opacity 160ms ${EASE_OUT}`, }} className={`relative inline-flex items-center justify-center font-medium rounded-l-full transition-colors cursor-pointer disabled:cursor-not-allowed ${s.actionPad} ${v.action} ${INTERACTION_FOCUS}`} > {loading ? ( <span className="inline-flex size-3 animate-spin rounded-full border-[1.5px] border-current border-r-transparent mr-1.5" /> ) : ( icon && <span className="inline-flex shrink-0 mr-1.5">{icon}</span> )} <span className="whitespace-nowrap">{label}</span> </button> {/* Hairline Divider */} <span aria-hidden="true" className={`w-px my-1.5 self-stretch ${v.divider}`} /> {/* Dropdown Chevron Trigger */} <button ref={triggerRef} type="button" disabled={inert} onClick={toggleMenu} aria-label={menuLabel} aria-haspopup="menu" aria-expanded={open} className={`relative inline-flex items-center justify-center rounded-r-full cursor-pointer transition-colors disabled:cursor-not-allowed ${s.triggerPad} ${v.trigger} ${INTERACTION_FOCUS}`} > <ChevronDown size={s.iconSize} strokeWidth={1.75} className="transition-transform duration-200 motion-reduce:transition-none" style={{ transform: open ? "rotate(180deg)" : "rotate(0deg)", transitionTimingFunction: EASE_SETTLE, }} /> </button> {/* Floating Aligned Dropdown Menu */} {open && ( <div ref={menuRef} role="menu" aria-label={menuLabel} onKeyDown={handleMenuKeyDown} style={{ transformOrigin: "top right", animation: `ui-fade-up 160ms ${EASE_OUT} forwards`, }} className="absolute right-0 top-[calc(100%+6px)] z-50 min-w-[170px] max-w-[calc(100vw-32px)] rounded-[12px] border border-border bg-popover p-1.5 text-popover-foreground shadow-[0_2px_12px_rgba(0,0,0,0.06)]" > {menuItems.map((item) => ( <button key={item.id} role="menuitem" data-menu-item="" type="button" disabled={item.disabled} onClick={() => { setOpen(false); item.onSelect(); }} className={`flex w-full items-center gap-2 rounded-[8px] px-2.5 py-1.5 text-[12.5px] font-medium transition-colors cursor-pointer disabled:cursor-not-allowed disabled:opacity-45 ${ item.destructive ? "text-red-600 dark:text-red-400 hover:bg-red-500/10" : "text-foreground hover:bg-muted" } ${INTERACTION_FOCUS}`} > {item.icon && ( <span className="inline-flex size-3.5 shrink-0 items-center justify-center text-muted-foreground"> {item.icon} </span> )} <span className="truncate">{item.label}</span> </button> ))} </div> )} </div> );}export default SplitButton;