Counter

nothing for now

0

20 is the max number.

Overview

The Counter component is a simple counter that allows users to increase or decrease a number between 0 and 20. It features smooth animations for the number changes and shake animations when the user tries to increase or decrease beyond the allowed range (0 or 20). The component uses framer-motion for animations and includes controls with plus and minus buttons.

Features

  • Increment/Decrement: Increase or decrease the number displayed, from 0 to 20.
  • Smooth Animations: The number smoothly transitions with an animation when the counter changes.
  • Shake Effect: The counter shakes when the user attempts to go below 0 or above 20.
  • Customizable Button Styles: Buttons are designed using Tailwind CSS and are responsive with hover and active states.

Dependencies

  • framer-motion: For handling animations.
  • lucide-react: For the icons used in the counter (Plus and Minus).
  • cn: A utility function for conditional class names (from the @/lib/utils directory).

Installation

Install the necessary dependencies:

npm install framer-motion lucide-react
"use client";
 
import React, { useState } from "react";
import {
  AnimatePresence,
  motion,
  useAnimate,
  type Variants,
} from "framer-motion";
import { cn } from "@/lib/utils";
import { Minus, Plus } from "lucide-react";
 
const animation: Variants = {
  hidden: (direction: -1 | 1) => ({
    y: direction === 1 ? 30 : -30,
    opacity: 0,
    filter: "blur(4px)",
  }),
  visible: {
    y: 0,
    opacity: 1,
    filter: "blur(0px)",
  },
  exit: (direction: -1 | 1) => ({
    y: direction === 1 ? -30 : 30,
    opacity: 0,
    filter: "blur(4px)",
  }),
};
 
const Counter = () => {
  const [num, setNum] = useState(0);
  const [direction, setDirection] = useState(1);
 
  const [scope, animate] = useAnimate();
 
  const handleShake = () => {
    animate(scope.current, { x: [0, 5, -5, 0] }, { duration: 0.2 });
  };
 
  const counter = (action: "decrease" | "increase") => {
    if (action === "decrease") {
      if (num === 0) return handleShake();
      setNum(num - 1);
      setDirection(-1);
    } else if (action === "increase") {
      if (num === 20) return handleShake();
      setNum(num + 1);
      setDirection(1);
    }
  };
 
  return (
    <div className="size-full flex flex-col items-center justify-center gap-8">
      <div
        ref={scope}
        className="flex items-center justify-center gap-8 text-4xl"
      >
        <button
          onClick={() => counter("decrease")}
          className={cn(
            "bg-box flex h-14 w-14 items-center justify-center rounded-full text-xl active:scale-90",
            num === 0 && "opacity-50"
          )}
        >
          <Minus />
        </button>
        <h3 className="w-12 text-center">
          <AnimatePresence mode="popLayout" custom={direction}>
            {num
              .toString()
              .split("")
              .map((value, index) => (
                <motion.span
                  key={`${value} ${index}`}
                  variants={animation}
                  initial="hidden"
                  animate="visible"
                  exit="exit"
                  custom={direction}
                  className="inline-block "
                >
                  {value}
                </motion.span>
              ))}
          </AnimatePresence>
        </h3>
        <button
          onClick={() => counter("increase")}
          className={cn(
            "bg-box flex h-14 w-14 items-center justify-center rounded-full text-xl active:scale-90",
            num === 20 && "opacity-50"
          )}
        >
          <Plus />
        </button>
      </div>
      <p className="text-muted-2">20 is the max number.</p>
    </div>
  );
};
 
export default Counter;

Usage

import Counter from "@/components/Counter";
 
function App() {
  return (
    <div>
      <Counter />
    </div>
  );
}

State Management

The component uses two state variables:

  • num: The current value of the counter.
  • direction: Tracks the direction of the counter change (either incrementing or decrementing).

Animation Variants

The animation constant defines the motion for the number changes:

  • hidden: The initial state for the number (before it animates into view).
  • visible: The visible state after the number is animated in.
  • exit: The state when the number exits (used for smooth transitions when the value is updated).

Each variant animates the position (y), opacity, and applies a blur effect to create a smooth transition.

Button Actions

  • Decrease: The "Minus" button decreases the num state by 1, unless it is at 0. If the number is already 0, it triggers a shake effect instead of decreasing.
  • Increase: The "Plus" button increases the num state by 1, unless it is at 20. If the number is already 20, it triggers a shake effect instead of increasing.

Shake Effect

When the user tries to go below 0 or above 20, the counter shakes using the animate function from framer-motion. The x position of the counter element is animated back and forth to create the shake effect.

Rendering the Number

The number is split into individual digits, and each digit is wrapped in a motion.span element that is animated using the animation variants. The AnimatePresence component handles the entrance and exit of the digits smoothly when the number changes.

Button Styling

Buttons are styled using Tailwind CSS, and conditional classes are added:

  • Active State: The buttons scale down when clicked, creating an active button effect.
  • Opacity: When the counter reaches 0 or 20, the respective button's opacity is reduced to indicate that the action is not possible.

Max Limit

The component displays a message below the counter indicating that 20 is the maximum number.

Props

This component does not accept any props but relies on its internal state to manage the counter.

Styling

The component uses Tailwind CSS for styling. Some key classes:

  • bg-box: Defines the background color of the buttons.
  • h-14 w-14: Sets the size of the buttons.
  • rounded-full: Makes the buttons circular.
  • text-xl: Sets the text size for the button icons.
  • active:scale-90: Creates a scale effect when the button is pressed.
  • text-muted-2: Provides a muted color for the max limit message.

Credits

Built by Bossadi Zenith