Family Popover Menu
April 2024
A popover menu, greatly inspired by the one I really enjoy on the Family app.
import {EnvelopeClosedIcon,GearIcon,HeartIcon,PlusIcon,} from "@radix-ui/react-icons";import { AnimatePresence, motion } from "framer-motion";import React, { useState } from "react";import useClickOutside from "hooks/useClickOutside";import { useMediaQuery } from "hooks/useMediaQuery";export default function FamilyPopoverMenu() {const refMenu = React.useRef<HTMLDivElement>(null);const [openMenu, setOpenMenu] = useState(false);const isScreenSizeSm = useMediaQuery("(max-width: 640px)");const duration = 0.2;const transition = { duration, ease: [0.32, 0.72, 0, 1] };const menuVariants = {open: {opacity: 1,width: isScreenSizeSm ? "100%" : "320px",height: 220,borderRadius: "16px",bottom: -44,transition,},closed: {bottom: 0,opacity: 1,width: "48px",height: 48,borderRadius: "50%",transition,},};const contentVariants = {open: { opacity: 1, scale: 1, transition },closed: { opacity: 0, scale: 1, transition },};const buttonVariants = {open: {opacity: 0,transition: {duration: duration / 2,},},closed: {opacity: 1,transition: {duration: duration,},},};const items = [{title: "Settings",text: "Adjust your preferences",icon: GearIcon,},{title: "Messages",text: "View your messages",icon: EnvelopeClosedIcon,},{title: "Favorites",text: "Manage your favorites",icon: HeartIcon,},];useClickOutside<HTMLDivElement>(refMenu, () => {setOpenMenu(false);});return (<div className="relative mx-6 mb-16 flex h-[300px] w-full items-end justify-start"><AnimatePresence>{openMenu && (<motion.divclassName="absolute bottom-0 left-0 flex flex-col items-center overflow-hidden bg-mauve-dark-1 p-1 dark:bg-mauve-light-1"initial="closed"animate="open"exit="closed"variants={menuVariants}onClick={(e) => e.stopPropagation()}ref={refMenu}><motion.ulvariants={contentVariants}className="relative flex w-full flex-col space-y-1">{items.map((item, index) => {return (<likey={index}className="w-full select-none rounded-b-[4px] rounded-t-[4px] bg-mauve-dark-2 transition-transform first:rounded-t-[12px] last:rounded-b-[12px] active:scale-[0.98] dark:bg-mauve-light-2"><div className="flex items-center py-3"><div className="px-4"><item.icon className="h-5 w-5 text-mauve-dark-12 dark:text-mauve-light-12" /></div><div><h3 className="text-base text-mauve-dark-12 dark:text-mauve-light-12">{item.title}</h3><p className="text-sm text-mauve-dark-11 dark:text-mauve-light-11">{item.text}</p></div></div></li>);})}</motion.ul></motion.div>)}</AnimatePresence><motion.buttonclassName="absolute bottom-0 left-0 flex h-12 w-12 items-center justify-center rounded-full bg-mauve-dark-1 p-2 text-mauve-dark-12 outline-none dark:bg-mauve-light-1 dark:text-mauve-light-12"disabled={openMenu}onClick={(e) => {e.stopPropagation();setOpenMenu(true);}}variants={buttonVariants}initial="closed"animate={openMenu ? "open" : "closed"}whileTap={{ scale: 0.95 }}><PlusIcon className="h-6 w-6" /></motion.button></div>);}