Action Menu

A sophisticated floating action button (FAB) that expands into a staggered vertical menu. It leverages AnimatePresence and spring physics for tactile transitions, featuring a reactive trigger that rotates upon interaction to provide clear visual feedback in a minimalist B/W layout.

framer-motionlucide-react

Installation

Install the required dependencies:

npm install framer-motion lucide-react
components/ui/action-menu.tsx
"use client";
  import React, { useState } from "react";
  import { motion, AnimatePresence } from "framer-motion";
  import { Plus, Image, Link, Type, MoreHorizontal } from "lucide-react";
  const ACTIONS = [
    {
      icon: <Type size={18} />,
      label: "Text",
      color: "bg-zinc-500 dark:bg-zinc-800",
    },
    {
      icon: <Image size={18} />,
      label: "Media",
      color: "bg-zinc-100 dark:bg-zinc-800",
    },
    {
      icon: <Link size={18} />,
      label: "Link",
      color: "bg-zinc-100 dark:bg-zinc-800",
    },
  ];
  
  export default function ActionMenu({ actions = ACTIONS }) {
    const [isOpen, setIsOpen] = useState(false);
  
    return (
      <div className=" flex flex-col items-end gap-4 z-50">
        <AnimatePresence>
          {isOpen && (
            <div className="flex flex-col gap-3">
              {actions.map((action, i) => (
                <motion.button
                  key={action.label}
                  initial={{ opacity: 0, scale: 0.5, y: 20 }}
                  animate={{ opacity: 1, scale: 1, y: 0 }}
                  exit={{ opacity: 0, scale: 0.5, y: 20 }}
                  transition={{
                    delay: i * 0.05,
                    type: "spring",
                    stiffness: 300,
                    damping: 20,
                  }}
                  className={'flex items-center gap-3 p-3 rounded-2xl shadow-2xl border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 group'}
                >
                  <span className="text-[10px] font-black uppercase tracking-widest text-zinc-900 opacity-50 group-hover:opacity-100 transition-opacity">
                    {action.label}
                  </span>
                  <div className="text-zinc-900 dark:text-white">
                    {action.icon}
                  </div>
                </motion.button>
              ))}
            </div>
          )}
        </AnimatePresence>
  
        <motion.button
          onClick={() => setIsOpen(!isOpen)}
          whileTap={{ scale: 0.9 }}
          className="w-14 h-14 flex items-center justify-center rounded-2xl bg-zinc-900 dark:bg-white text-white dark:text-black shadow-2xl border border-zinc-800 dark:border-zinc-200"
        >
          <motion.div animate={{ rotate: isOpen ? 45 : 0 }}>
            <Plus size={24} />
          </motion.div>
        </motion.button>
      </div>
    );
  }