unsave-popup
karrix/unsave-popupFreeBlock
A popup component to confirm before discarding changes. Inspired by Discord, it will shake when the user tries to leave without saving.
Live preview
live · rendered by the registry
Rendered by karrix on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://karrix.dev/r/unsave-popup.jsonSource
1"use client";23import { Loader2, Save } from "lucide-react";4import { AnimatePresence, easeOut, motion, useAnimation } from "motion/react";5import { Button } from "@/components/ui/button";6import { useState, useEffect, useCallback, useMemo, memo } from "react";7import { cn } from "@/lib/utils";8import React from "react";910const UnsavePopupDescription = memo(11 ({ children }: { children: React.ReactNode }) => {12 return <div className="flex flex-row items-center gap-2">{children}</div>;13 }14);15UnsavePopupDescription.displayName = "UnsavePopupDescription";1617const UnsavePopupAction = memo(18 ({19 children,20 isLoading,21 onClick,22 }: {23 children: React.ReactNode;24 isLoading?: boolean;25 onClick?: () => Promise<void>;26 }) => {27 return (28 <Button onClick={onClick} disabled={isLoading} className="rounded-lg">29 {isLoading ? (30 <span className="flex items-center gap-2">31 <Loader2 className="h-4 w-4 animate-spin" />32 Saving...33 </span>34 ) : (35 children36 )}37 </Button>38 );39 }40);41UnsavePopupAction.displayName = "UnsavePopupAction";4243const UnsavePopupDismiss = memo(44 ({45 children,46 onClick,47 }: {48 children: React.ReactNode;49 onClick?: () => void;50 }) => {51 return (52 <Button variant="ghost" onClick={onClick} className="rounded-lg">53 {children}54 </Button>55 );56 }57);58UnsavePopupDismiss.displayName = "UnsavePopupDismiss";5960// Main component61const UnsavePopup = memo(function UnsavePopup({62 children,63 onSave,64 onReset,65 shouldBlockFn,66 show,67 className,68}: {69 children: React.ReactNode;70 className?: string;71 onSave?: () => Promise<void>;72 onReset?: () => void;73 shouldBlockFn?: () => boolean;74 show: boolean;75}) {76 const controls = useAnimation();77 const [isLoading, setIsLoading] = useState(false);7879 const shakeAnimation = useCallback(80 () => ({81 x: [0, -8, 12, -15, 8, -10, 5, -3, 2, -1, 0],82 y: [0, 4, -9, 6, -12, 8, -3, 5, -2, 1, 0],83 filter: [84 "blur(0px)",85 "blur(2px)",86 "blur(2px)",87 "blur(3px)",88 "blur(2px)",89 "blur(2px)",90 "blur(1px)",91 "blur(2px)",92 "blur(1px)",93 "blur(1px)",94 "blur(0px)",95 ],96 "--warning-opacity": [0, 0.5, 0.3, 0.1, 0],97 transition: {98 duration: 0.4,99 ease: easeOut,100 },101 }),102 []103 );104105 const triggerShake = useCallback(async () => {106// … truncated
What it pulls in
npm packages
Other registry items
