shadimage

Pricing 1

Simple pricing block with single plan

index.tsx

"use client";

import SmoothButton from "@/components/smoothui/smooth-button";
import {
  ToggleGroupItem,
  ToggleGroupRoot,
} from "@/components/smoothui/toggle-group";
import { cn } from "@/lib/utils";
import { motion, useReducedMotion } from "motion/react";
import { useEffect, useRef, useState } from "react";

interface PriceFlowProps {
  className?: string;
  value: number;
}

function PriceFlow({ value, className = "" }: PriceFlowProps) {
  const [prevValue, setPrevValue] = useState(value);
  const prevTensRef = useRef<HTMLSpanElement>(null);
  const nextTensRef = useRef<HTMLSpanElement>(null);
  const prevOnesRef = useRef<HTMLSpanElement>(null);
  const nextOnesRef = useRef<HTMLSpanElement>(null);

  useEffect(() => {
    if (value !== prevValue) {
      const prevTens = prevTensRef.current;
      const nextTens = nextTensRef.current;
      const prevOnes = prevOnesRef.current;
      const nextOnes = nextOnesRef.current;

      if (
        prevTens &&
        nextTens &&
        Math.floor(value / 10) !== Math.floor(prevValue / 10)
      ) {
        if (Math.floor(value / 10) > Math.floor(prevValue / 10)) {
          prevTens.classList.add("slide-out-up");
          nextTens.classList.add("slide-in-up");
        } else {
          prevTens.classList.add("slide-out-down");
          nextTens.classList.add("slide-in-down");
        }

        const handleTensAnimationEnd = () => {
          prevTens.classList.remove("slide-out-up", "slide-out-down");
          nextTens.classList.remove("slide-in-up", "slide-in-down");
          prevTens.removeEventListener("animationend", handleTensAnimationEnd);
        };

        prevTens.addEventListener("animationend", handleTensAnimationEnd);
      }

      if (prevOnes && nextOnes && value % 10 !== prevValue % 10) {
        setTimeout(() => {
          if (value % 10 > prevValue % 10) {
            prevOnes.classList.add("slide-out-up");
            nextOnes.classList.add("slide-in-up");
          } else {
            prevOnes.classList.add("slide-out-down");
            nextOnes.classList.add("slide-in-down");
          }

          const handleOnesAnimationEnd = () => {
            prevOnes.classList.remove("slide-out-up", "slide-out-down");
            nextOnes.classList.remove("slide-in-up", "slide-in-down");
            prevOnes.removeEventListener(
              "animationend",
              handleOnesAnimationEnd
            );
          };

          prevOnes.addEventListener("animationend", handleOnesAnimationEnd);
        }, 50);
      }

      setPrevValue(value);
    }
  }, [value, prevValue]);

  const formatValue = (val: number) => val.toString().padStart(2, "0");
  const prevFormatted = formatValue(prevValue);
  const currentFormatted = formatValue(value);

  return (
    <span className={cn("relative inline-flex items-center", className)}>
      <span className="relative inline-block overflow-hidden">
        <span
          className="absolute inset-0 flex items-center justify-center"
          ref={prevTensRef}
          style={{ transform: "translateY(-100%)" }}
        >
          {prevFormatted[0]}
        </span>
        <span
          className="flex items-center justify-center"
          ref={nextTensRef}
          style={{ transform: "translateY(0%)" }}
        >
          {currentFormatted[0]}
        </span>
      </span>
      <span className="relative inline-block overflow-hidden">
        <span
          className="absolute inset-0 flex items-center justify-center"
          ref={prevOnesRef}
          style={{ transform: "translateY(-100%)" }}
        >
          {prevFormatted[1]}
        </span>
        <span
          className="flex items-center justify-center"
          ref={nextOnesRef}
          style={{ transform: "translateY(0%)" }}
        >
          {currentFormatted[1]}
        </span>
      </span>
    </span>
  );
}

export function PricingSimple() {
  const shouldReduceMotion = useReducedMotion();
  const [isAnnual, setIsAnnual] = useState(true);

  return (
    <section>
      <div className="relative bg-muted/50 py-16 md:py-32">
        <div className="mx-auto max-w-5xl px-6">
          <div className="mx-auto max-w-2xl text-center">
            <h2 className="text-balance font-bold text-3xl md:text-4xl lg:text-5xl lg:tracking-tight">
              Simple pricing for everyone
            </h2>
            <p className="mx-auto mt-4 max-w-xl text-balance text-foreground/70 text-lg">
              One plan, all features. No hidden fees, no complicated tiers.
            </p>
            <div className="my-12">
              <ToggleGroupRoot
                aria-label="Billing period"
                className="mx-auto rounded-full border bg-background p-1"
                onValueChange={(next) => {
                  if (next[0] === "annually") {
                    setIsAnnual(true);
                  } else if (next[0] === "monthly") {
                    setIsAnnual(false);
                  }
                }}
                type="single"
                value={[isAnnual ? "annually" : "monthly"]}
              >
                <ToggleGroupItem
                  className="w-24 rounded-full data-pressed:bg-brand data-pressed:text-white dark:data-pressed:bg-brand"
                  value="monthly"
                >
                  Monthly
                </ToggleGroupItem>
                <ToggleGroupItem
                  className="w-24 rounded-full data-pressed:bg-brand data-pressed:text-white dark:data-pressed:bg-brand"
                  value="annually"
                >
                  Annually
                </ToggleGroupItem>
              </ToggleGroupRoot>
              <div className="mt-3 text-center text-xs">
                <span className="font-medium text-brand">Save 20%</span> On
                Annual Billing
              </div>
            </div>
          </div>
          <div className="container">
            <div className="mx-auto max-w-md">
              <motion.div
                animate={
                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
                }
                className="group relative flex h-[650px] cursor-pointer flex-col overflow-hidden rounded-2xl border bg-background p-8"
                data-animate-card
                initial={
                  shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 40 }
                }
                transition={
                  shouldReduceMotion
                    ? { duration: 0 }
                    : { duration: 0.3, ease: [0.22, 1, 0.36, 1] }
                }
              >
                {/* Gradient Accent */}
                <div className="gradient-accent absolute top-0 right-0 h-4 w-32 rounded-bl-2xl bg-gradient-to-r from-green-400 via-blue-400 to-purple-400" />

                <div className="card-content relative z-10 flex h-full flex-col">
                  {/* Title */}
                  <h3 className="mb-4 font-bold text-2xl text-foreground">
                    Pro
                  </h3>
                  {/* Price & Duration */}
                  <div className="mb-6">
                    <span className="font-semibold text-3xl text-foreground">
                      <PriceFlow value={isAnnual ? 15 : 19} />€
                    </span>
                    <span className="mx-2 text-foreground/70">•</span>
                    <span className="text-foreground/70">
                      Perfect for individuals
                    </span>
                  </div>
                  {/* CTA Button */}
                  <SmoothButton className="mb-6 w-full" variant="candy">
                    Get Started
                  </SmoothButton>
                  {/* Description */}
                  <p className="mb-6 flex-grow text-foreground/70 text-sm leading-relaxed">
                    Everything you need to build and deploy amazing
                    applications. Simple, powerful, and affordable.
                  </p>
                  {/* What's Included */}
                  <div className="space-y-4">
                    <h4 className="font-medium text-foreground/70 text-xs uppercase tracking-wider">
                      What&apos;s included:
                    </h4>
                    <ul className="space-y-3">
                      {[
                        "Unlimited Projects",
                        "Email Support",
                        "All Features",
                        "Advanced Analytics",
                        "Team Collaboration",
                        "Custom Domains",
                        "Priority Updates",
                        "API Access",
                      ].map((item) => (
                        <li
                          className="flex items-center gap-3 text-foreground text-sm"
                          key={item}
                        >
                          <div className="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-foreground">
                            <svg
                              aria-hidden="true"
                              className="h-2 w-2 text-background"
                              fill="currentColor"
                              viewBox="0 0 20 20"
                            >
                              <path
                                clipRule="evenodd"
                                d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
                                fillRule="evenodd"
                              />
                            </svg>
                          </div>
                          {item}
                        </li>
                      ))}
                    </ul>
                  </div>
                </div>
              </motion.div>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

export default PricingSimple;
Where it goes
pricing
Use cases
saas-landing

DependenciesBundle weight · medium

animation
motion