{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hold-toconfirm",
  "type": "registry:ui",
  "title": "Hold ToConfirm",
  "description": "A button that requires holding down to confirm irreversible or critical actions.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": ["@scrollxui/button"],
  "dependencies": [
    "motion",
    "@radix-ui/react-slot",
    "class-variance-authority",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/hold-toconfirm.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport {\n  motion,\n  AnimatePresence,\n  useMotionValue,\n  animate,\n  useTransform,\n} from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { buttonVariants, type ButtonProps } from '@/components/ui/button';\n\ninterface HoldToConfirmProps extends Omit<ButtonProps, 'asChild'> {\n  asChild?: boolean;\n  duration?: number;\n  onConfirm?: () => void;\n  animation?: 'border' | 'fill';\n  fillClassName?: string;\n  confirmedChildren?: React.ReactNode;\n  confirmedClassName?: string;\n  resetAfter?: number;\n  showProgressOnConfirm?: boolean;\n}\n\nconst HoldToConfirm = React.forwardRef<HTMLButtonElement, HoldToConfirmProps>(\n  (\n    {\n      duration = 2000,\n      onConfirm,\n      animation = 'fill',\n      variant,\n      size,\n      className,\n      fillClassName,\n      confirmedChildren,\n      confirmedClassName,\n      resetAfter = 2000,\n      showProgressOnConfirm = false,\n      asChild = false,\n      children = 'Hold to confirm',\n      ...props\n    },\n    ref,\n  ) => {\n    const [confirmed, setConfirmed] = React.useState(false);\n    const textRef = React.useRef<HTMLSpanElement>(null);\n\n    const progress = useMotionValue(0);\n    const controlsRef = React.useRef<ReturnType<typeof animate> | null>(null);\n    const holdTimerRef = React.useRef<NodeJS.Timeout | null>(null);\n    const resetTimerRef = React.useRef<NodeJS.Timeout | null>(null);\n\n    const resetHold = (smooth: boolean) => {\n      controlsRef.current?.stop();\n      if (smooth) {\n        controlsRef.current = animate(progress, 0, {\n          duration: 0.3,\n          ease: 'easeOut',\n        });\n      } else {\n        progress.set(0);\n      }\n    };\n\n    const startHold = () => {\n      if (confirmed) return;\n\n      controlsRef.current = animate(progress, 1, {\n        duration: duration / 1000,\n        ease: 'linear',\n      });\n\n      holdTimerRef.current = setTimeout(() => {\n        setConfirmed(true);\n        onConfirm?.();\n\n        if (!showProgressOnConfirm) {\n          resetHold(false);\n        } else {\n          controlsRef.current?.stop();\n          progress.set(1);\n        }\n\n        if (resetAfter > 0) {\n          resetTimerRef.current = setTimeout(() => {\n            setConfirmed(false);\n            resetHold(true);\n          }, resetAfter);\n        }\n      }, duration);\n    };\n\n    const cancelHold = () => {\n      if (holdTimerRef.current) clearTimeout(holdTimerRef.current);\n      if (!confirmed) resetHold(true);\n    };\n\n    const width = useTransform(progress, [0, 1], ['0%', '100%']);\n    const borderClip = useTransform(\n      progress,\n      [0, 1],\n      ['inset(0 100% 0 0 round 0.375rem)', 'inset(0 0% 0 0 round 0.375rem)'],\n    );\n\n    const textProgress = useTransform(progress, (value) => {\n      if (!textRef.current) return 0;\n\n      const buttonRect = textRef.current\n        .closest('button')\n        ?.getBoundingClientRect();\n      const textRect = textRef.current.getBoundingClientRect();\n\n      if (!buttonRect || !textRect) return 0;\n\n      const textStartPercent =\n        (textRect.left - buttonRect.left) / buttonRect.width;\n      const fillPosition = value;\n\n      if (fillPosition <= textStartPercent) return 0;\n\n      const textWidth = textRect.width / buttonRect.width;\n      const adjustedProgress = (fillPosition - textStartPercent) / textWidth;\n\n      return Math.min(Math.max(adjustedProgress, 0), 1);\n    });\n\n    const textWidth = useTransform(textProgress, [0, 1], ['0%', '100%']);\n\n    React.useEffect(() => {\n      return () => {\n        controlsRef.current?.stop();\n        if (holdTimerRef.current) clearTimeout(holdTimerRef.current);\n        if (resetTimerRef.current) clearTimeout(resetTimerRef.current);\n      };\n    }, []);\n\n    const Comp = asChild ? 'span' : 'button';\n\n    return (\n      <Comp\n        ref={ref}\n        {...props}\n        onMouseDown={startHold}\n        onMouseUp={cancelHold}\n        onMouseLeave={cancelHold}\n        onTouchStart={startHold}\n        onTouchEnd={cancelHold}\n        className={cn(\n          'relative overflow-hidden',\n          buttonVariants({ variant, size }),\n          className,\n        )}\n      >\n        {animation === 'fill' && (!confirmed || showProgressOnConfirm) && (\n          <motion.div\n            className={cn('absolute left-0 top-0 h-full', fillClassName)}\n            style={{ width }}\n          />\n        )}\n\n        {animation === 'border' && (!confirmed || showProgressOnConfirm) && (\n          <motion.div\n            className={cn(\n              'absolute inset-0 border-2 rounded-md pointer-events-none',\n              fillClassName,\n            )}\n            style={{ clipPath: borderClip }}\n          />\n        )}\n\n        <span className='relative z-10 flex items-center justify-center w-full'>\n          <AnimatePresence mode='wait'>\n            {confirmed && confirmedChildren ? (\n              <motion.span\n                key='confirmed'\n                initial={{ opacity: 0, scale: 0.9, y: 8 }}\n                animate={{ opacity: 1, scale: 1, y: 0 }}\n                exit={{ opacity: 0, scale: 0.9, y: -8 }}\n                transition={{ duration: 0.3 }}\n                className={cn('flex items-center gap-2', confirmedClassName)}\n              >\n                {confirmedChildren}\n              </motion.span>\n            ) : (\n              <motion.span\n                key='default'\n                initial={{ opacity: 0, scale: 0.9 }}\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0, scale: 0.9 }}\n                transition={{ duration: 0.25 }}\n                className='relative flex items-center gap-2'\n              >\n                <span ref={textRef} className='relative'>\n                  {children}\n                  {animation === 'fill' && (\n                    <motion.span\n                      className={cn(\n                        'absolute inset-0 overflow-hidden',\n                        fillClassName?.includes('text-')\n                          ? fillClassName\n                          : 'text-white dark:text-black',\n                      )}\n                      style={{ width: textWidth }}\n                    >\n                      {children}\n                    </motion.span>\n                  )}\n                </span>\n              </motion.span>\n            )}\n          </AnimatePresence>\n        </span>\n      </Comp>\n    );\n  },\n);\n\nHoldToConfirm.displayName = 'HoldToConfirm';\n\nexport { HoldToConfirm };\n"
    },
    {
      "type": "registry:ui",
      "path": "components/ui/button.tsx",
      "content": "import * as React from 'react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nimport { cn } from '@/lib/utils';\n\nconst buttonVariants = cva(\n  'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',\n  {\n    variants: {\n      variant: {\n        default:\n          'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90',\n        destructive:\n          'bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90',\n        outline:\n          'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',\n        secondary:\n          'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',\n        ghost: 'hover:bg-accent hover:text-accent-foreground',\n        link: 'text-primary underline-offset-4 hover:underline',\n        success: 'bg-green-600 text-white hover:bg-green-700',\n        warning: 'bg-yellow-500 text-black hover:bg-yellow-600',\n        info: 'bg-blue-500 text-white hover:bg-blue-600',\n        dark: 'bg-gray-800 text-white hover:bg-gray-700',\n        light: 'bg-gray-100 text-gray-800 hover:bg-gray-200',\n        gradient:\n          'bg-linear-to-r from-indigo-500 via-purple-500 to-pink-500 text-white hover:opacity-90',\n        glass:\n          'bg-white/10 backdrop-blur-md text-white border border-white/20 hover:bg-white/20',\n      },\n      size: {\n        default: 'h-9 px-4 py-2',\n        sm: 'h-8 rounded-md px-3 text-xs',\n        lg: 'h-10 rounded-md px-8',\n        icon: 'h-9 w-9',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends\n    React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : 'button';\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nButton.displayName = 'Button';\n\nexport { Button, buttonVariants };\n"
    },
    {
      "type": "registry:lib",
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}"
    }
  ]
}
