price-flow

PreviousNext

A PriceFlow component for SmoothUI.

Docs
smoothuiui

Preview

Loading preview…
index.tsx
"use client";

import { cn } from "@repo/shadcn-ui/lib/utils";
import { useEffect, useRef, useState } from "react";

const STAGGER_DELAY = 50;

export type PriceFlowProps = {
  value: number;
  className?: string;
};

const animateDigit = (
  prevElement: HTMLElement | null,
  nextElement: HTMLElement | null,
  isIncreasing: boolean
) => {
  if (prevElement === null || nextElement === null) {
    return;
  }

  if (isIncreasing) {
    prevElement.classList.add("slide-out-up");
    nextElement.classList.add("slide-in-up");
  } else {
    prevElement.classList.add("slide-out-down");
    nextElement.classList.add("slide-in-down");
  }

  const handleAnimationEnd = () => {
    prevElement.classList.remove("slide-out-up", "slide-out-down");
    nextElement.classList.remove("slide-in-up", "slide-in-down");
    prevElement.removeEventListener("animationend", handleAnimationEnd);
  };

  prevElement.addEventListener("animationend", handleAnimationEnd);
};

export default function PriceFlow({ value, className = "" }: PriceFlowProps) {
  const [prevValue, setPrevValue] = useState(value);

  // Create refs for each digit position (tens and ones)
  const prevTensRef = useRef<HTMLElement>(null);
  const nextTensRef = useRef<HTMLElement>(null);
  const prevOnesRef = useRef<HTMLElement>(null);
  const nextOnesRef = useRef<HTMLElement>(null);

  useEffect(() => {
    if (value === prevValue) {
      return;
    }

    const prevTens = prevTensRef.current;
    const nextTens = nextTensRef.current;
    const prevOnes = prevOnesRef.current;
    const nextOnes = nextOnesRef.current;

    const prevTensValue = Math.floor(prevValue / 10);
    const currentTensValue = Math.floor(value / 10);
    const tensChanged = currentTensValue !== prevTensValue;

    if (tensChanged && prevTens && nextTens) {
      const isTensIncreasing = currentTensValue > prevTensValue;
      animateDigit(prevTens, nextTens, isTensIncreasing);
    }

    const prevOnesValue = prevValue % 10;
    const currentOnesValue = value % 10;
    const onesChanged = currentOnesValue !== prevOnesValue;

    if (onesChanged && prevOnes && nextOnes) {
      const isOnesIncreasing = currentOnesValue > prevOnesValue;
      setTimeout(() => {
        animateDigit(prevOnes, nextOnes, isOnesIncreasing);
      }, STAGGER_DELAY);
    }

    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">
        {/* Tens digit */}
        <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">
        {/* Ones digit */}
        <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>
  );
}

Installation

npx shadcn@latest add @smoothui/price-flow

Usage

import { PriceFlow } from "@/components/ui/price-flow"
<PriceFlow />