{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "unsave-popup",
  "type": "registry:block",
  "description": "A popup component to confirm before discarding changes. Inspired by Discord, it will shake when the user tries to leave without saving.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "registry/kl-ui/unsave-popup/unsave-popup.tsx",
      "content": "\"use client\";\n\nimport { Loader2, Save } from \"lucide-react\";\nimport { AnimatePresence, easeOut, motion, useAnimation } from \"motion/react\";\nimport { Button } from \"@/components/ui/button\";\nimport { useState, useEffect, useCallback, useMemo, memo } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport React from \"react\";\n\nconst UnsavePopupDescription = memo(\n  ({ children }: { children: React.ReactNode }) => {\n    return <div className=\"flex flex-row items-center gap-2\">{children}</div>;\n  }\n);\nUnsavePopupDescription.displayName = \"UnsavePopupDescription\";\n\nconst UnsavePopupAction = memo(\n  ({\n    children,\n    isLoading,\n    onClick,\n  }: {\n    children: React.ReactNode;\n    isLoading?: boolean;\n    onClick?: () => Promise<void>;\n  }) => {\n    return (\n      <Button onClick={onClick} disabled={isLoading} className=\"rounded-lg\">\n        {isLoading ? (\n          <span className=\"flex items-center gap-2\">\n            <Loader2 className=\"h-4 w-4 animate-spin\" />\n            Saving...\n          </span>\n        ) : (\n          children\n        )}\n      </Button>\n    );\n  }\n);\nUnsavePopupAction.displayName = \"UnsavePopupAction\";\n\nconst UnsavePopupDismiss = memo(\n  ({\n    children,\n    onClick,\n  }: {\n    children: React.ReactNode;\n    onClick?: () => void;\n  }) => {\n    return (\n      <Button variant=\"ghost\" onClick={onClick} className=\"rounded-lg\">\n        {children}\n      </Button>\n    );\n  }\n);\nUnsavePopupDismiss.displayName = \"UnsavePopupDismiss\";\n\n// Main component\nconst UnsavePopup = memo(function UnsavePopup({\n  children,\n  onSave,\n  onReset,\n  shouldBlockFn,\n  show,\n  className,\n}: {\n  children: React.ReactNode;\n  className?: string;\n  onSave?: () => Promise<void>;\n  onReset?: () => void;\n  shouldBlockFn?: () => boolean;\n  show: boolean;\n}) {\n  const controls = useAnimation();\n  const [isLoading, setIsLoading] = useState(false);\n\n  const shakeAnimation = useCallback(\n    () => ({\n      x: [0, -8, 12, -15, 8, -10, 5, -3, 2, -1, 0],\n      y: [0, 4, -9, 6, -12, 8, -3, 5, -2, 1, 0],\n      filter: [\n        \"blur(0px)\",\n        \"blur(2px)\",\n        \"blur(2px)\",\n        \"blur(3px)\",\n        \"blur(2px)\",\n        \"blur(2px)\",\n        \"blur(1px)\",\n        \"blur(2px)\",\n        \"blur(1px)\",\n        \"blur(1px)\",\n        \"blur(0px)\",\n      ],\n      \"--warning-opacity\": [0, 0.5, 0.3, 0.1, 0], \n      transition: {\n        duration: 0.4,\n        ease: easeOut,\n      },\n    }),\n    []\n  );\n\n  const triggerShake = useCallback(async () => {\n    await controls.start(shakeAnimation());\n  }, [controls, shakeAnimation]);\n\n  const handleSave = useCallback(async () => {\n    setIsLoading(true);\n    await onSave?.();\n    setIsLoading(false);\n  }, [onSave]);\n\n  const handleReset = useCallback(() => {\n    onReset?.();\n  }, [onReset]);\n\n  const { hasCompoundComponents, hasValidComponents } = useMemo(() => {\n    const childrenArray = React.Children.toArray(children);\n    const hasCompound = childrenArray.some(\n      (child) =>\n        React.isValidElement(child) &&\n        (child.type === UnsavePopupDescription ||\n          child.type === UnsavePopupAction ||\n          child.type === UnsavePopupDismiss)\n    );\n\n    if (!hasCompound) {\n      return { hasCompoundComponents: false, hasValidComponents: true };\n    }\n\n    const hasDescription = childrenArray.some(\n      (child) =>\n        React.isValidElement(child) && child.type === UnsavePopupDescription\n    );\n    const hasAction = childrenArray.some(\n      (child) => React.isValidElement(child) && child.type === UnsavePopupAction\n    );\n    const hasDismiss = childrenArray.some(\n      (child) =>\n        React.isValidElement(child) && child.type === UnsavePopupDismiss\n    );\n\n    return {\n      hasCompoundComponents: true,\n      hasValidComponents: hasDescription && hasAction && hasDismiss,\n    };\n  }, [children]);\n\n  useEffect(() => {\n    if (hasCompoundComponents && !hasValidComponents) {\n      throw new Error(\n        cn(\n          \"When using UnsavePopupDescription, UnsavePopupAction, or UnsavePopupDismiss, \",\n          \"you must use all three components together. Check out the docs for more info.\"\n        )\n      );\n    }\n  }, [hasCompoundComponents, hasValidComponents]);\n\n  const defaultButtons = useCallback(\n    () => (\n      <div className=\"flex flex-row items-center gap-1\">\n        <Button variant=\"ghost\" onClick={handleReset} className=\"rounded-lg\">\n          Reset\n        </Button>\n        <Button\n          onClick={handleSave}\n          disabled={isLoading}\n          className=\"rounded-lg\"\n        >\n          {isLoading ? (\n            <span className=\"flex items-center gap-2\">\n              <Loader2 className=\"h-4 w-4 animate-spin\" />\n              Saving...\n            </span>\n          ) : (\n            <span className=\"flex items-center gap-2\">\n              <Save className=\"h-4 w-4\" />\n              Save\n            </span>\n          )}\n        </Button>\n      </div>\n    ),\n    [isLoading, handleReset, handleSave]\n  );\n\n  useEffect(() => {\n    if (shouldBlockFn && shouldBlockFn()) {\n      triggerShake();\n    }\n  }, [shouldBlockFn, triggerShake]);\n\n  return (\n    <AnimatePresence>\n      {show && (\n        <motion.div\n          initial={{ y: 100, opacity: 0 }}\n          animate={{ y: 0, opacity: 1 }}\n          exit={{ y: 100, opacity: 0 }}\n          transition={{ type: \"spring\", bounce: 0.5, duration: 0.5 }}\n          className=\"fixed right-0 bottom-4 left-0 z-50 mx-auto w-fit\"\n        >\n          <motion.div\n            animate={controls}\n            className={cn(\n              \"flex w-96 flex-row items-center justify-between gap-2 rounded-lg\",\n              \"border border-gray-200 bg-white dark:border-zinc-700\",\n              \"dark:bg-gradient-to-br dark:from-zinc-900 dark:to-zinc-950\",\n              \"px-4 py-2 text-sm shadow-md\",\n              \"relative before:absolute before:inset-0 before:rounded-lg\",\n              \"before:bg-red-500 [--warning-opacity:0]\",\n              \"before:opacity-[var(--warning-opacity)]\",\n              \"before:pointer-events-none\",\n              className\n            )}\n          >\n            {hasCompoundComponents ? (\n              children\n            ) : (\n              <>\n                <div className=\"flex flex-row items-center gap-2\">\n                  {children}\n                </div>\n                {defaultButtons()}\n              </>\n            )}\n          </motion.div>\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n});\n\nUnsavePopup.displayName = \"UnsavePopup\";\n\nexport {\n  UnsavePopup,\n  UnsavePopupDescription,\n  UnsavePopupAction,\n  UnsavePopupDismiss,\n};\n",
      "type": "registry:component",
      "target": "components/kl-ui/unsave-popup.tsx"
    }
  ]
}