"use client";import * as React from "react";import { INTERACTION_FOCUS, useInteractionReducedMotion,} from "./ui-interaction-motion";export type IconMorphMode = "rotate" | "scale" | "flip";export interface IconMorphProps extends React.HTMLAttributes<HTMLSpanElement> { /** The first icon (rendered when active is false). */ from: React.ReactNode; /** The second icon (rendered when active is true). */ to: React.ReactNode; /** Controls which icon is active. */ active?: boolean; /** Morph animation style. Defaults to scale. */ mode?: IconMorphMode; /** Icon bounding box size in pixels. Defaults to 16. */ size?: number;}export interface IconMorphButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> { from: React.ReactNode; to: React.ReactNode; active?: boolean; onActiveChange?: (next: boolean) => void; mode?: IconMorphMode; size?: "sm" | "md" | "lg"; variant?: "outline" | "ghost" | "solid"; label: string;}const EASE = "cubic-bezier(0.16, 1, 0.3, 1)";const EASE_SETTLE = "cubic-bezier(0.22, 1.18, 0.36, 1)";/** * Standalone kinetic SVG icon morph primitive. */export function IconMorph({ from, to, active = false, mode = "scale", size = 16, className = "", style, ...props}: IconMorphProps) { const reduced = useInteractionReducedMotion(); // Mode-specific transforms const getTransforms = () => { if (reduced) { return { from: { opacity: active ? 0 : 1, transform: "none", }, to: { opacity: active ? 1 : 0, transform: "none", }, }; } if (mode === "rotate") { return { from: { opacity: active ? 0 : 1, transform: active ? "rotate(90deg) scale(0.65)" : "rotate(0deg) scale(1)", transition: `opacity 220ms ${EASE}, transform 320ms ${EASE_SETTLE}`, }, to: { opacity: active ? 1 : 0, transform: active ? "rotate(0deg) scale(1)" : "rotate(-90deg) scale(0.65)", transition: `opacity 220ms ${EASE}, transform 320ms ${EASE_SETTLE}`, }, }; } if (mode === "flip") { return { from: { opacity: active ? 0 : 1, transform: active ? "rotateY(180deg) scale(0.7)" : "rotateY(0deg) scale(1)", transition: `opacity 180ms ${EASE}, transform 360ms ${EASE_SETTLE}`, }, to: { opacity: active ? 1 : 0, transform: active ? "rotateY(0deg) scale(1)" : "rotateY(-180deg) scale(0.7)", transition: `opacity 180ms ${EASE}, transform 360ms ${EASE_SETTLE}`, }, }; } // Default "scale" with soft blur return { from: { opacity: active ? 0 : 1, transform: active ? "scale(0.55)" : "scale(1)", filter: active ? "blur(1.5px)" : "blur(0px)", transition: `opacity 200ms ${EASE}, transform 300ms ${EASE_SETTLE}, filter 200ms ${EASE}`, }, to: { opacity: active ? 1 : 0, transform: active ? "scale(1)" : "scale(0.55)", filter: active ? "blur(0px)" : "blur(1.5px)", transition: `opacity 200ms ${EASE}, transform 300ms ${EASE_SETTLE}, filter 200ms ${EASE}`, }, }; }; const transforms = getTransforms(); return ( <span className={`relative inline-flex items-center justify-center shrink-0 isolate select-none ${className}`} style={{ width: size, height: size, perspective: mode === "flip" ? 600 : undefined, ...style, }} aria-hidden="true" {...props} > <span className="absolute inset-0 flex items-center justify-center will-change-transform" style={transforms.from} > {from} </span> <span className="absolute inset-0 flex items-center justify-center will-change-transform" style={transforms.to} > {to} </span> </span> );}const buttonSizeStyles = { sm: "size-7 rounded-full text-[12px]", md: "size-8.5 rounded-full text-[13px]", lg: "size-10 rounded-full text-[14px]",};const iconSizes = { sm: 13, md: 15, lg: 17,};const buttonVariantStyles = { outline: "bg-card text-foreground shadow-[inset_0_0_0_1px_var(--border)] hover:bg-accent active:scale-[0.96]", ghost: "bg-transparent text-muted-foreground hover:bg-accent hover:text-foreground active:scale-[0.96]", solid: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-[0_1px_2px_rgba(48,93,222,0.18)] active:scale-[0.96]",};/** * Clickable interactive button housing the IconMorph primitive. */export function IconMorphButton({ from, to, active: controlledActive, onActiveChange, mode = "scale", size = "md", variant = "outline", label, className = "", onClick, ...props}: IconMorphButtonProps) { const [internalActive, setInternalActive] = React.useState(false); const active = controlledActive !== undefined ? controlledActive : internalActive; const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { const next = !active; if (controlledActive === undefined) { setInternalActive(next); } onActiveChange?.(next); onClick?.(e); }; const sizeClass = buttonSizeStyles[size]; const variantClass = buttonVariantStyles[variant]; const iconPx = iconSizes[size]; return ( <button type="button" aria-label={label} aria-pressed={active} onClick={handleClick} className={`inline-flex items-center justify-center cursor-pointer transition-colors duration-150 select-none ${sizeClass} ${variantClass} ${INTERACTION_FOCUS} ${className}`} {...props} > <IconMorph from={from} to={to} active={active} mode={mode} size={iconPx} /> </button> );}export default IconMorph;