{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button-58",
  "title": "Hold to delete",
  "description": "A fill crosses the button while it is held for two seconds, snaps back on release and turns into a confirmation.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/button.json",
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/button/button-58.tsx",
      "content": "'use client';\n\nimport {\n  AnimatePresence,\n  motion,\n  type Transition,\n  type Variants,\n} from 'motion/react';\nimport { useEffect, useState } from 'react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { CheckIcon, TrashIcon } from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Button } from '@/registry/ndeko/ui/button';\n\ntype HoldStatus = 'idle' | 'holding' | 'deleted';\n\nconst HOLD_DURATION_MS = 2000;\nconst DELETED_RESET_MS = 1800;\nconst HOLD_KEYS = new Set([' ', 'Enter']);\n\nconst FILL_LAYER =\n  'bg-destructive pointer-events-none absolute inset-0 flex items-center justify-center rounded-[inherit] px-3.5 text-white';\n\nconst FILL_BY_CLIP = [\n  '[clip-path:inset(0_100%_0_0)] transition-[clip-path] duration-200 ease-(--ease-out-strong)',\n  'group-data-filled/button:[clip-path:inset(0)] group-data-filled/button:duration-2000 group-data-filled/button:ease-linear',\n].join(' ');\n\nconst FILL_BY_FADE = [\n  'opacity-0 transition-opacity duration-200 ease-(--ease-out-strong)',\n  'group-data-filled/button:opacity-100 group-data-filled/button:duration-2000 group-data-filled/button:ease-linear',\n].join(' ');\n\nconst CONTENT =\n  'col-start-1 row-start-1 flex items-center justify-center gap-2';\n\nconst SWAP_TRANSITION: Transition = { duration: 0.15, ease: [0.2, 0, 0, 1] };\n\nconst SWAP_VARIANTS: Variants = {\n  enter: { opacity: 0, scale: 0.6, filter: 'blur(2px)' },\n  visible: { opacity: 1, scale: 1, filter: 'blur(0px)' },\n  exit: { opacity: 0, scale: 0.6, filter: 'blur(2px)' },\n};\n\nconst FADE_VARIANTS: Variants = {\n  enter: { opacity: 0 },\n  visible: { opacity: 1 },\n  exit: { opacity: 0 },\n};\n\nfunction swapVariantsOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return FADE_VARIANTS;\n  }\n  return SWAP_VARIANTS;\n}\n\nfunction fillMotionOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return FILL_BY_FADE;\n  }\n  return FILL_BY_CLIP;\n}\n\nfunction contentOf(isDeleted: boolean) {\n  if (isDeleted) {\n    return { key: 'deleted', icon: <CheckIcon />, label: 'Deleted' };\n  }\n  return { key: 'hold', icon: <TrashIcon />, label: 'Hold to delete' };\n}\n\nfunction HoldContent({\n  isDeleted,\n  variants,\n}: {\n  isDeleted: boolean;\n  variants: Variants;\n}) {\n  const content = contentOf(isDeleted);\n\n  return (\n    <span className='grid'>\n      <span aria-hidden='true' className={cn(CONTENT, 'invisible')}>\n        <TrashIcon />\n        Hold to delete\n      </span>\n      <AnimatePresence mode='popLayout' initial={false}>\n        <motion.span\n          key={content.key}\n          className={CONTENT}\n          variants={variants}\n          initial='enter'\n          animate='visible'\n          exit='exit'\n          transition={SWAP_TRANSITION}\n        >\n          {content.icon}\n          {content.label}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\nfunction preventContextMenu(event: React.MouseEvent) {\n  event.preventDefault();\n}\n\nexport default function Button58() {\n  const [status, setStatus] = useState<HoldStatus>('idle');\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const swapVariants = swapVariantsOf(shouldReduceMotion);\n  const isHolding = status === 'holding';\n  const isDeleted = status === 'deleted';\n  const isFilled = status !== 'idle';\n\n  useEffect(() => {\n    if (!isHolding) {\n      return;\n    }\n    const timeout = window.setTimeout(() => {\n      setStatus('deleted');\n    }, HOLD_DURATION_MS);\n    return () => window.clearTimeout(timeout);\n  }, [isHolding]);\n\n  useEffect(() => {\n    if (!isDeleted) {\n      return;\n    }\n    const timeout = window.setTimeout(() => {\n      setStatus('idle');\n    }, DELETED_RESET_MS);\n    return () => window.clearTimeout(timeout);\n  }, [isDeleted]);\n\n  function startHold() {\n    if (status !== 'idle') {\n      return;\n    }\n    setStatus('holding');\n  }\n\n  function cancelHold() {\n    if (!isHolding) {\n      return;\n    }\n    setStatus('idle');\n  }\n\n  function pressPointer(event: React.PointerEvent<HTMLButtonElement>) {\n    const isPrimaryButton = event.button === 0;\n    if (!isPrimaryButton) {\n      return;\n    }\n    startHold();\n  }\n\n  function pressKey(event: React.KeyboardEvent<HTMLButtonElement>) {\n    const isHoldKey = HOLD_KEYS.has(event.key);\n    if (!isHoldKey) {\n      return;\n    }\n    event.preventDefault();\n    if (event.repeat) {\n      return;\n    }\n    startHold();\n  }\n\n  function releaseKey(event: React.KeyboardEvent<HTMLButtonElement>) {\n    const isHoldKey = HOLD_KEYS.has(event.key);\n    if (!isHoldKey) {\n      return;\n    }\n    cancelHold();\n  }\n\n  return (\n    <>\n      <Button\n        variant='outline'\n        data-filled={isFilled || undefined}\n        className='touch-manipulation [-webkit-touch-callout:none]'\n        onPointerDown={pressPointer}\n        onPointerUp={cancelHold}\n        onPointerLeave={cancelHold}\n        onPointerCancel={cancelHold}\n        onKeyDown={pressKey}\n        onKeyUp={releaseKey}\n        onBlur={cancelHold}\n        onContextMenu={preventContextMenu}\n      >\n        <HoldContent isDeleted={isDeleted} variants={swapVariants} />\n        <span\n          aria-hidden='true'\n          className={cn(FILL_LAYER, fillMotionOf(shouldReduceMotion))}\n        >\n          <HoldContent isDeleted={isDeleted} variants={swapVariants} />\n        </span>\n      </Button>\n      <span className='sr-only' role='status'>\n        {isDeleted && 'Project deleted'}\n      </span>\n    </>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "components",
    "button"
  ],
  "type": "registry:component"
}