{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"undo-bar","type":"registry:ui","title":"Undo Bar","description":"A snackbar for reversible actions: it springs up after a delete while a ring counts down the seconds left to undo. Hovering or focusing it pauses the clock.","dependencies":["motion"],"registryDependencies":["utils"],"files":[{"path":"registry/ui/undo-bar.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { AnimatePresence, MotionConfig, animate, motion, useMotionValue, useTransform, type AnimationPlaybackControls } from \"motion/react\";\nimport { useEffect, useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface UndoBarProps {\n  /** Show the bar. Set it again to restart the countdown. */\n  open: boolean;\n  /** What just happened, e.g. \"Message deleted\". */\n  message: string;\n  /** Called when Undo is pressed. */\n  onUndo?: () => void;\n  /** Called when the countdown runs out without an undo. */\n  onExpire?: () => void;\n  /** Seconds before the action is final. */\n  duration?: number;\n  /** Undo button text. */\n  actionLabel?: string;\n  /** \"fixed\" pins it to the viewport; \"absolute\" keeps it inside the nearest positioned parent. */\n  position?: \"fixed\" | \"absolute\";\n  className?: string;\n}\n\nconst R = 9;\nconst C = 2 * Math.PI * R;\n\n/**\n * A snackbar for reversible actions: it rises in after a delete, a ring counts\n * down the seconds left to undo, and hovering or focusing it pauses the clock.\n */\nexport function UndoBar({\n  open,\n  message,\n  onUndo,\n  onExpire,\n  duration = 5,\n  actionLabel = \"Undo\",\n  position = \"fixed\",\n  className,\n}: UndoBarProps) {\n  const progress = useMotionValue(1);\n  const dash = useTransform(progress, (p) => C * (1 - p));\n  const seconds = useTransform(progress, (p) => Math.max(1, Math.ceil(p * duration)));\n  const clock = useRef<AnimationPlaybackControls | null>(null);\n  const expire = useRef(onExpire);\n\n  useEffect(() => {\n    expire.current = onExpire;\n  }, [onExpire]);\n\n  useEffect(() => {\n    if (!open) return;\n    progress.set(1);\n    const c = animate(progress, 0, { duration, ease: \"linear\", onComplete: () => expire.current?.() });\n    clock.current = c;\n    return () => c.stop();\n  }, [open, duration, message, progress]);\n\n  const pause = () => clock.current?.pause();\n  const resume = () => clock.current?.play();\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <div\n        className={cn(\n          \"pointer-events-none inset-x-0 bottom-4 z-50 flex justify-center px-4\",\n          position === \"fixed\" ? \"fixed\" : \"absolute\",\n          className,\n        )}\n      >\n        <AnimatePresence>\n          {open && (\n            <motion.div\n              key=\"undo-bar\"\n              role=\"status\"\n              aria-live=\"polite\"\n              onPointerEnter={pause}\n              onPointerLeave={resume}\n              onFocus={pause}\n              onBlur={resume}\n              className=\"pointer-events-auto flex items-center gap-3 rounded-full border border-border bg-foreground py-1.5 pl-2 pr-1.5 text-sm text-background shadow-2xl\"\n              initial={{ y: 24, opacity: 0, scale: 0.92, filter: \"blur(4px)\" }}\n              animate={{ y: 0, opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n              exit={{ y: 16, opacity: 0, scale: 0.96, filter: \"blur(4px)\", transition: { duration: 0.18 } }}\n              transition={{ type: \"spring\", bounce: 0.3, duration: 0.45 }}\n            >\n              <span className=\"relative grid size-7 place-items-center\" aria-hidden>\n                <svg viewBox=\"0 0 24 24\" className=\"absolute inset-0 size-full -rotate-90\">\n                  <circle cx=\"12\" cy=\"12\" r={R} fill=\"none\" stroke=\"currentColor\" strokeOpacity=\"0.2\" strokeWidth=\"2\" />\n                  <motion.circle\n                    cx=\"12\"\n                    cy=\"12\"\n                    r={R}\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth=\"2\"\n                    strokeLinecap=\"round\"\n                    strokeDasharray={C}\n                    style={{ strokeDashoffset: dash }}\n                  />\n                </svg>\n                <motion.span className=\"text-[11px] font-semibold tabular-nums\">{seconds}</motion.span>\n              </span>\n              <span className=\"whitespace-nowrap font-medium\">{message}</span>\n              <button\n                type=\"button\"\n                onClick={onUndo}\n                className=\"rounded-full bg-background/15 px-3 py-1 text-sm font-semibold transition-colors hover:bg-background/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-background/70\"\n              >\n                {actionLabel}\n              </button>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n    </MotionConfig>\n  );\n}\n"}]}