Native Start Now

PreviousNext

Animated button with sparkle effects, loading states, and smooth transitions for starting actions. (Base UI)

Docs
uitripledcomponent

Preview

Loading preview…
components/native/baseui/native-start-now-baseui.tsx
"use client";

import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "framer-motion";
import { ArrowRight, Loader2, Rocket } from "lucide-react";
import { ReactNode, useState } from "react";
import { NativeButton } from "./native-button-baseui";

export interface NativeStartNowProps {
  /**
   * Callback when start button is clicked
   */
  onStart: () => void | Promise<void>;
  /**
   * Text to show on the button
   * Default: "Start Now"
   */
  label?: string;
  /**
   * Loading text during async action
   * Default: "Starting..."
   */
  loadingLabel?: string;
  /**
   * Success text after completion
   * Default: "Let's Go!"
   */
  successLabel?: string;
  /**
   * Size variant
   * Default: "md"
   */
  size?: "sm" | "md" | "lg";
  /**
   * Show sparkle animation on hover
   * Default: true
   */
  showRocket?: boolean;
  /**
   * Icon to use for Rocket and success state
   * Default: Rocket icon
   */
  icon?: ReactNode;
  /**
   * Additional class names for the container
   */
  className?: string;
  /**
   * Disable the button
   */
  disabled?: boolean;
  /**
   * Variant style
   * Default: "gradient"
   */
  variant?: "gradient" | "solid" | "outline";
}

const sizeVariants = {
  sm: "h-9 px-4 text-sm",
  default: "h-11 px-6 text-base",
  lg: "h-14 px-8 text-lg",
};

const sizeMap = {
  sm: "sm" as const,
  md: "default" as const,
  lg: "lg" as const,
};

const iconSizeVariants = {
  sm: "h-3.5 w-3.5",
  md: "h-4 w-4",
  lg: "h-5 w-5",
  default: "h-4 w-4",
};

export function NativeStartNow({
  onStart,
  label = "Start Now",
  loadingLabel = "Starting...",
  successLabel = "Let's Go!",
  size = "md",
  showRocket = true,
  icon,
  className,
  disabled = false,
  variant = "gradient",
}: NativeStartNowProps) {
  const [status, setStatus] = useState<"idle" | "loading" | "success">("idle");
  const [isHovered, setIsHovered] = useState(false);

  const handleClick = async () => {
    if (disabled || status !== "idle") return;

    setStatus("loading");
    try {
      await onStart();
      setStatus("success");
      setTimeout(() => setStatus("idle"), 2000);
    } catch (error) {
      setStatus("idle");
    }
  };

  const getButtonStyles = () => {
    const baseStyles =
      "relative overflow-hidden font-semibold transition-all duration-300";

    switch (variant) {
      case "gradient":
        return cn(
          baseStyles,
          "bg-gradient-to-r from-primary via-primary/90 to-primary text-primary-foreground",
          "hover:shadow-lg hover:shadow-primary/50",
          "border-0"
        );
      case "solid":
        return cn(
          baseStyles,
          "bg-primary text-primary-foreground",
          "hover:bg-primary/90"
        );
      case "outline":
        return cn(
          baseStyles,
          "border-2 border-primary text-primary bg-background",
          "hover:bg-primary hover:text-primary-foreground"
        );
      default:
        return baseStyles;
    }
  };

  return (
    <motion.div
      className={cn("relative inline-flex", className)}
      onHoverStart={() => setIsHovered(true)}
      onHoverEnd={() => setIsHovered(false)}
    >
      {/* Sparkle particles */}
      <AnimatePresence>
        {showRocket && isHovered && status === "idle" && (
          <>
            {[...Array(6)].map((_, i) => (
              <motion.div
                key={i}
                initial={{
                  opacity: 0,
                  scale: 0,
                  x: 0,
                  y: 0,
                }}
                animate={{
                  opacity: [0, 1, 0],
                  scale: [0, 1, 0],
                  x: Math.cos((i * Math.PI) / 3) * 40,
                  y: Math.sin((i * Math.PI) / 3) * 40,
                }}
                exit={{ opacity: 0 }}
                transition={{
                  duration: 1,
                  repeat: Number.POSITIVE_INFINITY,
                  delay: i * 0.1,
                  ease: "easeOut",
                }}
                className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
              >
                <div className="h-3 w-3 text-primary fill-primary">
                  {icon || (
                    <Rocket className="h-3 w-3 text-primary fill-primary" />
                  )}
                </div>
              </motion.div>
            ))}
          </>
        )}
      </AnimatePresence>

      <NativeButton
        onClick={handleClick}
        disabled={disabled || status !== "idle"}
        loading={false}
        size={size === "md" ? "default" : size === "sm" ? "sm" : "lg"}
        variant={
          variant === "gradient"
            ? "default"
            : variant === "solid"
              ? "default"
              : "outline"
        }
        className={cn(
          sizeVariants[size === "md" ? "default" : size],
          getButtonStyles(),
          disabled && "opacity-50 cursor-not-allowed",
          "rounded-md shadow-md"
        )}
      >
        {/* Shimmer effect */}
        {variant === "gradient" && status === "idle" && (
          <motion.div
            className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent"
            animate={{
              x: ["-200%", "200%"],
            }}
            transition={{
              duration: 2,
              repeat: Number.POSITIVE_INFINITY,
              repeatDelay: 1,
              ease: "easeInOut",
            }}
          />
        )}

        {/* Button content */}
        <AnimatePresence mode="wait">
          {status === "idle" && (
            <motion.div
              key="idle"
              initial={{ opacity: 0, y: 10, filter: "blur(4px)" }}
              animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
              exit={{ opacity: 0, y: -10, filter: "blur(4px)" }}
              transition={{ duration: 0.2 }}
              className="flex items-center gap-2 relative z-10"
            >
              {label}
              <motion.div
                animate={{
                  x: [0, 3, 0],
                }}
                transition={{
                  duration: 1.2,
                  repeat: Number.POSITIVE_INFINITY,
                  ease: "easeInOut",
                }}
              >
                <ArrowRight
                  className={iconSizeVariants[size === "md" ? "md" : size]}
                />
              </motion.div>
            </motion.div>
          )}

          {status === "loading" && (
            <motion.div
              key="loading"
              initial={{ opacity: 0, y: 10, filter: "blur(4px)" }}
              animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
              exit={{ opacity: 0, y: -10, filter: "blur(4px)" }}
              transition={{ duration: 0.2 }}
              className="flex items-center gap-2 relative z-10"
            >
              <Loader2
                className={cn(
                  iconSizeVariants[size === "md" ? "md" : size],
                  "animate-spin"
                )}
              />
              {loadingLabel}
            </motion.div>
          )}

          {status === "success" && (
            <motion.div
              key="success"
              initial={{ opacity: 0, scale: 0.8, filter: "blur(4px)" }}
              animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
              exit={{ opacity: 0, scale: 0.8, filter: "blur(4px)" }}
              transition={{
                type: "spring",
                stiffness: 300,
                damping: 20,
              }}
              className="flex items-center gap-2 relative z-10"
            >
              <motion.div
                animate={{
                  rotate: [0, 360],
                }}
                transition={{
                  duration: 0.5,
                }}
                className={cn(
                  iconSizeVariants[size === "md" ? "md" : size],
                  "fill-current"
                )}
              >
                {icon || (
                  <Rocket
                    className={cn(
                      iconSizeVariants[size === "md" ? "md" : size],
                      "fill-current"
                    )}
                  />
                )}
              </motion.div>
              {successLabel}
            </motion.div>
          )}
        </AnimatePresence>
      </NativeButton>
    </motion.div>
  );
}

Installation

npx shadcn@latest add @uitripled/native-start-now-baseui

Usage

import { NativeStartNowBaseui } from "@/components/native-start-now-baseui"
<NativeStartNowBaseui />