"use client"; import React, { useState, useMemo, useRef, useLayoutEffect, useEffect, } from "react"; import { motion, AnimatePresence, useReducedMotion, type HTMLMotionProps, } from "motion/react"; import { cn } from "@/lib/utils"; export type SparkleButtonSize = "sm" | "default" | "lg"; export type SparkleButtonVariant = "default" | "outline" | "glass"; export type BlurAnimateBy = "letters" | "words"; export type BlurDirection = "top" | "bottom"; export interface SparkleButtonProps extends Omit< HTMLMotionProps<"button">, "children" > { variant?: SparkleButtonVariant; text?: string; activeText?: string; loading?: boolean; onLoadingChange?: (loading: boolean) => void; size?: SparkleButtonSize; icon?: React.ReactNode; animateBy?: BlurAnimateBy; direction?: BlurDirection; delay?: number; stepDuration?: number; dissolveDuration?: number; springStiffness?: number; springDamping?: number; } const sizeStyles: Record = { sm: "h-8 px-4 text-xs gap-2 [&_svg]:size-3.5", default: "h-10 px-5 text-sm gap-2.5 [&_svg]:size-4", lg: "h-12 px-7 text-base gap-3 [&_svg]:size-5", }; const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect; const buildKeyframes = ( from: Record, steps: Array>, ): Record> => { const keys = new Set([ ...Object.keys(from), ...steps.flatMap((s) => Object.keys(s)), ]); const keyframes: Record> = {}; keys.forEach((k) => { keyframes[k] = [from[k], ...steps.map((s) => s[k])]; }); return keyframes; }; export const SparkleButton = React.forwardRef< HTMLButtonElement, SparkleButtonProps >( ( { className, variant = "default", text = "Generate Magic", activeText = "Generating...", loading, onLoadingChange, size = "default", icon, animateBy = "letters", direction = "top", delay = 40, stepDuration = 0.35, dissolveDuration = 0.2, springStiffness = 350, springDamping = 28, onClick, disabled, ...props }, ref, ) => { const reduceMotion = useReducedMotion(); const [internalLoading, setInternalLoading] = useState(false); const isControlled = loading !== undefined; const isLoading = isControlled ? loading : internalLoading; const idleMeasureRef = useRef(null); const activeMeasureRef = useRef(null); const [measuredWidths, setMeasuredWidths] = useState<{ idle: number; active: number; }>({ idle: 0, active: 0 }); const updateMeasurements = () => { if (idleMeasureRef.current && activeMeasureRef.current) { const idleW = Math.ceil( idleMeasureRef.current.getBoundingClientRect().width, ); const activeW = Math.ceil( activeMeasureRef.current.getBoundingClientRect().width, ); setMeasuredWidths({ idle: idleW, active: activeW }); } }; useIsomorphicLayoutEffect(() => { updateMeasurements(); if (typeof document !== "undefined" && "fonts" in document) { document.fonts.ready.then(updateMeasurements); } window.addEventListener("resize", updateMeasurements); return () => window.removeEventListener("resize", updateMeasurements); }, [text, activeText, size]); const targetWidth = isLoading ? measuredWidths.active : measuredWidths.idle; const currentTargetText = isLoading ? activeText : text; const segments = useMemo(() => { if (animateBy === "words") { return currentTargetText.split(" "); } return currentTargetText.split(""); }, [currentTargetText, animateBy]); const defaultFrom = useMemo( () => direction === "top" ? { filter: "blur(10px)", opacity: 0, y: -12 } : { filter: "blur(10px)", opacity: 0, y: 12 }, [direction], ); const defaultTo = useMemo( () => [ { filter: "blur(4px)", opacity: 0.65, y: direction === "top" ? 2 : -2, }, { filter: "blur(0px)", opacity: 1, y: 0 }, ], [direction], ); const stepCount = defaultTo.length + 1; const times = useMemo( () => Array.from({ length: stepCount }, (_, i) => stepCount === 1 ? 0 : i / (stepCount - 1), ), [stepCount], ); const animateKeyframes = useMemo( () => buildKeyframes(defaultFrom, defaultTo), [defaultFrom, defaultTo], ); const handleClick = (e: React.MouseEvent) => { if (disabled) return; if (!isControlled) { setInternalLoading((prev) => !prev); } onLoadingChange?.(!isLoading); onClick?.(e); }; const getVariantStyles = () => { switch (variant) { case "outline": return cn( "bg-white text-zinc-900 border border-zinc-200 shadow-xs hover:bg-zinc-50 hover:border-zinc-300 hover:shadow-sm focus-visible:ring-zinc-400 focus-visible:ring-offset-background", "dark:bg-zinc-950/80 dark:text-zinc-100 dark:border-white/15 dark:shadow-none dark:hover:bg-zinc-900 dark:hover:border-white/25 dark:focus-visible:ring-white/40 dark:focus-visible:ring-offset-black", "after:opacity-0", ); case "glass": return cn( "bg-zinc-100/80 text-zinc-900 border border-zinc-300/80 backdrop-blur-md shadow-[inset_0_1px_1px_rgba(255,255,255,0.9),0_4px_14px_rgba(0,0,0,0.06)] hover:bg-zinc-100 hover:border-zinc-400 focus-visible:ring-zinc-400 focus-visible:ring-offset-background", "dark:bg-white/6 dark:text-white dark:border-white/15 dark:backdrop-blur-md dark:shadow-[inset_0_1px_1px_rgba(255,255,255,0.15),0_4px_16px_rgba(0,0,0,0.5)] dark:hover:bg-white/10 dark:hover:border-white/25 dark:focus-visible:ring-white/40 dark:focus-visible:ring-offset-black", "after:opacity-0", ); default: return cn( "bg-zinc-900 text-white border-zinc-800 shadow-[inset_0px_1px_1px_0px_rgba(255,255,255,0.25),0px_4px_14px_-2px_rgba(0,0,0,0.22),0px_1px_2px_0px_rgba(0,0,0,0.12)] hover:bg-black hover:border-zinc-700 hover:shadow-[inset_0px_1px_1.5px_0px_rgba(255,255,255,0.35),0px_8px_20px_-4px_rgba(0,0,0,0.28)] focus-visible:ring-zinc-950/40 focus-visible:ring-offset-background", "dark:bg-[#0d0d0f] dark:text-white dark:border-white/15 dark:shadow-[inset_0px_1px_1px_0px_rgba(255,255,255,0.22),inset_0px_2px_3px_0px_rgba(255,255,255,0.1),inset_0px_-2px_4px_0px_rgba(0,0,0,0.5),0px_4px_16px_-2px_rgba(0,0,0,0.8),0px_1px_2px_0px_rgba(0,0,0,0.4)] dark:hover:border-white/30 dark:hover:shadow-[inset_0px_1px_1.5px_0px_rgba(255,255,255,0.35),inset_0px_2px_4px_0px_rgba(255,255,255,0.15),inset_0px_-2px_4px_0px_rgba(0,0,0,0.5),0px_8px_24px_-4px_rgba(0,0,0,0.9),0px_0px_16px_0px_rgba(255,255,255,0.06)] dark:hover:brightness-105 dark:focus-visible:ring-white/40 dark:focus-visible:ring-offset-black", "after:bg-[linear-gradient(90deg,transparent,rgba(255,255,255,0.55),transparent)]", ); } }; return (
{currentTargetText}
); }, ); SparkleButton.displayName = "SparkleButton"; export default SparkleButton;