UI Elements

Add Member

A waitlist join form with an email input, a submit button, and a stacked avatar row showing recent members.

add-member.tsx
"use client";

import { motion as m, useReducedMotion, animate } from "motion/react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ArrowRight } from "lucide-react";
import { useEffect, useState } from "react";

const easeOut = [0.23, 1, 0.32, 1] as const;

const AVATARS = [1, 2, 3, 4, 5].map((n) => `https://i.pravatar.cc/150?img=${n}`);
const TOTAL = 180;

const NewMember = () => {
  const reduce = useReducedMotion();
  const [count, setCount] = useState(reduce ? TOTAL : 0);

  useEffect(() => {
    if (reduce) return;
    const controls = animate(0, TOTAL, {
      duration: 1.4,
      ease: easeOut,
      onUpdate: (v) => setCount(Math.round(v)),
    });
    return () => controls.stop();
  }, [reduce]);

  return (
    <div className="flex h-full w-full items-center justify-center p-6">
      <div className="flex w-full max-w-sm flex-col gap-7">
        {/* Waitlist input */}
        <div className="flex items-center gap-2">
          <Input
            placeholder="Enter your email"
            aria-label="Email address"
            className="h-10 bg-muted/40"
          />
          <Button className="h-10 shrink-0 gap-1.5 transition-transform duration-150 active:scale-[0.97]">
            Join waitlist
            <ArrowRight className="size-4" />
          </Button>
        </div>

        {/* Social proof */}
        <m.div
          initial={reduce ? false : { opacity: 0, y: 8 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.5, ease: easeOut }}
          className="flex items-center justify-between gap-4"
        >
          {/* Avatar stack */}
          <div className="flex -space-x-3">
            {AVATARS.map((src, i) => (
              <m.img
                src={src}
                key={i}
                alt=""
                draggable={false}
                initial={reduce ? false : { opacity: 0, x: -8, scale: 0.8 }}
                animate={{ opacity: 1, x: 0, scale: 1 }}
                transition={{ duration: 0.4, delay: 0.15 + i * 0.06, ease: easeOut }}
                className="relative size-9 rounded-full object-cover ring-2 ring-background outline outline-1 -outline-offset-1 outline-white/10 transition-transform duration-200 hover:z-10 hover:-translate-y-1"
              />
            ))}
          </div>

          {/* Counter pill with proud badge */}
          <div className="relative flex shrink-0 items-center rounded-full border border-border/60 bg-muted/40 py-2 pl-16 pr-5">
            <span
              aria-hidden
              className="absolute -left-1 top-1/2 flex size-12 -translate-y-1/2 items-center justify-center rounded-full bg-primary text-sm font-semibold tabular-nums text-primary-foreground shadow-md shadow-black/20 ring-4 ring-background"
            >
              {count}
            </span>
            <span className="flex items-center gap-1.5 whitespace-nowrap text-sm text-muted-foreground">
              People joined
              <span className="inline-flex items-center gap-1.5 font-medium text-foreground">
                <span className="relative flex size-1.5">
                  <span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-500/70 motion-reduce:hidden" />
                  <span className="relative inline-flex size-1.5 rounded-full bg-emerald-500" />
                </span>
                today
              </span>
            </span>
          </div>
        </m.div>
      </div>
    </div>
  );
};

export default NewMember;