@thibault.sh/hooks
Version: 
A comprehensive collection of React hooks for browser storage, UI interactions, and more
1 lines • 9.54 kB
Source Map (JSON)
{"version":3,"sources":["../src/hooks/useLongPress.ts"],"names":["useLongPress","options","delay","preventContext","onPress","onLongPress","onLongPressCanceled","timeout","useRef","startTime","frameRef","pressState","setPressState","useState","updateProgress","useCallback","elapsed","newProgress","prev","__spreadProps","__spreadValues","useEffect","start","event","end","cancel"],"mappings":"iGAsEO,SAASA,CAAaC,CAAAA,CAAAA,CAA4B,EAAC,CAAG,CAC3D,GAAM,CAAE,KAAAC,CAAAA,GAAAA,CAAQ,GAAK,CAAA,cAAA,CAAAC,CAAiB,CAAA,CAAA,CAAA,CAAM,QAAAC,CAAS,CAAA,WAAA,CAAAC,CAAa,CAAA,mBAAA,CAAAC,CAAoB,CAAIL,CAAAA,CAAAA,CAEpFM,CAAUC,CAAAA,MAAAA,GACVC,CAAYD,CAAAA,MAAAA,CAAe,CAAC,CAAA,CAC5BE,CAAWF,CAAAA,MAAAA,EAEX,CAAA,CAACG,EAAYC,CAAa,CAAA,CAAIC,QAAqB,CAAA,CACvD,UAAW,CACX,CAAA,CAAA,aAAA,CAAe,CACf,CAAA,CAAA,QAAA,CAAU,CACZ,CAAC,CAAA,CAMKC,CAAiBC,CAAAA,WAAAA,CAAY,IAAM,CACvC,GAAI,CAACN,CAAAA,CAAU,SAAW,CAACE,CAAAA,CAAW,SAAW,CAAA,OAEjD,IAAMK,CAAU,CAAA,IAAA,CAAK,GAAI,EAAA,CAAIP,EAAU,OACjCQ,CAAAA,CAAAA,CAAc,IAAK,CAAA,GAAA,CAAID,CAAUd,CAAAA,GAAAA,CAAO,CAAC,CAAA,CAE/CU,EAAeM,GAAUC,EAAAA,CAAAA,CAAAC,CAAA,CAAA,EAAA,CAAKF,KAAL,CAAW,QAAA,CAAUD,CAAY,CAAA,CAAE,EAExDA,CAAc,CAAA,CAAA,GAChBP,CAAS,CAAA,OAAA,CAAU,qBAAsBI,CAAAA,CAAc,CAE3D,EAAA,CAAA,CAAG,CAACZ,GAAOS,CAAAA,CAAAA,CAAW,SAAS,CAAC,EAGhCU,SAAU,CAAA,IACD,IAAM,CACPX,EAAS,OAAS,EAAA,oBAAA,CAAqBA,CAAS,CAAA,OAAO,CACvDH,CAAAA,CAAAA,CAAQ,OAAS,EAAA,YAAA,CAAaA,EAAQ,OAAO,EACnD,CACC,CAAA,EAAE,CAML,CAAA,IAAMe,CAAQP,CAAAA,WAAAA,CACXQ,GAA+C,CAC1CpB,CAAAA,EACFoB,CAAM,CAAA,cAAA,GAGRd,CAAU,CAAA,OAAA,CAAU,IAAK,CAAA,GAAA,GACzBG,CAAc,CAAA,CAAE,SAAW,CAAA,CAAA,CAAA,CAAM,cAAe,CAAO,CAAA,CAAA,QAAA,CAAU,CAAE,CAAC,EACpEF,CAAS,CAAA,OAAA,CAAU,qBAAsBI,CAAAA,CAAc,CAEvDP,CAAAA,CAAAA,CAAQ,OAAU,CAAA,UAAA,CAAW,IAAM,CACjCK,CAAAA,CAAeM,CAAUC,EAAAA,CAAAA,CAAAC,EAAA,EAAKF,CAAAA,CAAAA,CAAAA,CAAL,CAAW,aAAA,CAAe,EAAK,CAAE,CAAA,CAAA,CAC1Db,CAAA,EAAA,IAAA,EAAAA,CACF,GAAA,CAAA,CAAGH,GAAK,EACV,EACA,CAACA,GAAAA,CAAOG,CAAaF,CAAAA,CAAAA,CAAgBW,CAAc,CACrD,CAAA,CAMMU,CAAMT,CAAAA,WAAAA,CACTQ,GAA+C,CAC1Cb,CAAAA,CAAS,OAAS,EAAA,oBAAA,CAAqBA,CAAS,CAAA,OAAO,CACvDH,CAAAA,CAAAA,CAAQ,SAAS,YAAaA,CAAAA,CAAAA,CAAQ,OAAO,CAAA,CAE5BI,EAAW,aAG1BA,GAAAA,CAAAA,CAAW,QAAW,CAAA,CAAA,GACxBL,GAAA,IAAAA,EAAAA,CAAAA,EAAAA,CAAAA,CAEFF,CAAA,EAAA,IAAA,EAAAA,KAGFQ,CAAc,CAAA,CAAE,SAAW,CAAA,CAAA,CAAA,CAAO,cAAe,CAAO,CAAA,CAAA,QAAA,CAAU,CAAE,CAAC,EACvE,CACA,CAAA,CAACN,CAAqBF,CAAAA,CAAAA,CAASO,EAAW,aAAeA,CAAAA,CAAAA,CAAW,QAAQ,CAC9E,CAMMc,CAAAA,CAAAA,CAASV,WACZQ,CAAAA,CAAAA,EAA+C,CAC1Cb,CAAS,CAAA,OAAA,EAAS,oBAAqBA,CAAAA,CAAAA,CAAS,OAAO,CACvDH,CAAAA,CAAAA,CAAQ,OAAS,EAAA,YAAA,CAAaA,EAAQ,OAAO,CAAA,CAE7CI,CAAW,CAAA,SAAA,EAAa,CAACA,CAAAA,CAAW,aACtCL,GAAAA,CAAAA,EAAA,MAAAA,CAGFM,EAAAA,CAAAA,CAAAA,CAAAA,CAAc,CAAE,SAAA,CAAW,GAAO,aAAe,CAAA,CAAA,CAAA,CAAO,QAAU,CAAA,CAAE,CAAC,EACvE,CAAA,CACA,CAACN,CAAAA,CAAqBK,CAAW,CAAA,SAAA,CAAWA,CAAW,CAAA,aAAa,CACtE,CAEA,CAAA,OAAO,CAEL,QAAA,CAAU,CACR,WAAaW,CAAAA,CAAAA,CACb,SAAWE,CAAAA,CAAAA,CACX,aAAcC,CACd,CAAA,YAAA,CAAcH,CACd,CAAA,UAAA,CAAYE,EACZ,aAAeC,CAAAA,CACjB,CAEA,CAAA,KAAA,CAAO,CACL,SAAWd,CAAAA,CAAAA,CAAW,SACtB,CAAA,aAAA,CAAeA,EAAW,aAC1B,CAAA,QAAA,CAAUA,CAAW,CAAA,QACvB,CACF,CACF","file":"useLongPress.mjs","sourcesContent":["import { useCallback, useRef, useState, useEffect } from \"react\";\n\n/**\n * Configuration options for the useLongPress hook\n */\ninterface LongPressOptions {\n  /** Duration in milliseconds before long press is triggered (default: 400) */\n  delay?: number;\n  /** Whether to disable context menu on long press (default: true) */\n  preventContext?: boolean;\n  /** Callback fired when a normal press (shorter than delay) is completed */\n  onPress?: () => void;\n  /** Callback fired when a long press is successfully triggered */\n  onLongPress?: () => void;\n  /** Callback fired when a long press is canceled before completion */\n  onLongPressCanceled?: () => void;\n}\n\n/**\n * Internal state for tracking press status and progress\n */\ninterface PressState {\n  /** Whether the element is currently being pressed */\n  isPressed: boolean;\n  /** Whether the press has exceeded the long press threshold */\n  isLongPressed: boolean;\n  /** Progress towards completing a long press (0 to 1) */\n  progress: number;\n}\n\n/**\n * Hook that handles both normal press and long press interactions with progress tracking.\n *\n * Provides event handlers for detecting short taps vs long presses, with smooth progress\n * animation and customizable timing. Works with both mouse and touch events.\n *\n * @param options - Configuration options for the long press behavior\n * @param options.delay - Duration in milliseconds before triggering long press (default: 400)\n * @param options.preventContext - Whether to prevent context menu on long press (default: true)\n * @param options.onPress - Callback for normal press (when released before delay)\n * @param options.onLongPress - Callback for successful long press (when delay is reached)\n * @param options.onLongPressCanceled - Callback when long press is interrupted\n *\n * @returns Object containing:\n *   - `handlers`: Event handlers to spread on your element\n *   - `state`: Current press state with `isPressed`, `isLongPressed`, and `progress` (0-1)\n *\n * @example\n * ```tsx\n * function DeleteButton({ onDelete }) {\n *   const { handlers, state } = useLongPress({\n *     delay: 1000,\n *     onPress: () => console.log('Quick tap - no action'),\n *     onLongPress: onDelete,\n *     onLongPressCanceled: () => console.log('Canceled deletion')\n *   });\n *\n *   return (\n *     <button {...handlers} className={state.isPressed ? 'pressing' : ''}>\n *       {state.isLongPressed\n *         ? 'Deleting...'\n *         : `Hold to delete (${Math.round(state.progress * 100)}%)`\n *       }\n *     </button>\n *   );\n * }\n * ```\n *\n * @see https://thibault.sh/hooks/use-long-press\n */\nexport function useLongPress(options: LongPressOptions = {}) {\n  const { delay = 400, preventContext = true, onPress, onLongPress, onLongPressCanceled } = options;\n\n  const timeout = useRef<ReturnType<typeof setTimeout>>();\n  const startTime = useRef<number>(0);\n  const frameRef = useRef<number>();\n\n  const [pressState, setPressState] = useState<PressState>({\n    isPressed: false,\n    isLongPressed: false,\n    progress: 0,\n  });\n\n  /**\n   * Updates the progress of the long press animation\n   * Uses requestAnimationFrame for smooth updates\n   */\n  const updateProgress = useCallback(() => {\n    if (!startTime.current || !pressState.isPressed) return;\n\n    const elapsed = Date.now() - startTime.current;\n    const newProgress = Math.min(elapsed / delay, 1);\n\n    setPressState((prev) => ({ ...prev, progress: newProgress }));\n\n    if (newProgress < 1) {\n      frameRef.current = requestAnimationFrame(updateProgress);\n    }\n  }, [delay, pressState.isPressed]);\n\n  // Cleanup animation frames and timeouts on unmount\n  useEffect(() => {\n    return () => {\n      if (frameRef.current) cancelAnimationFrame(frameRef.current);\n      if (timeout.current) clearTimeout(timeout.current);\n    };\n  }, []);\n\n  /**\n   * Handles the start of a press interaction\n   * Initializes timers and starts progress tracking\n   */\n  const start = useCallback(\n    (event: React.TouchEvent | React.MouseEvent) => {\n      if (preventContext) {\n        event.preventDefault();\n      }\n\n      startTime.current = Date.now();\n      setPressState({ isPressed: true, isLongPressed: false, progress: 0 });\n      frameRef.current = requestAnimationFrame(updateProgress);\n\n      timeout.current = setTimeout(() => {\n        setPressState((prev) => ({ ...prev, isLongPressed: true }));\n        onLongPress?.();\n      }, delay);\n    },\n    [delay, onLongPress, preventContext, updateProgress]\n  );\n\n  /**\n   * Handles the end of a press interaction\n   * Determines if it was a long press and fires appropriate callbacks\n   */\n  const end = useCallback(\n    (event: React.TouchEvent | React.MouseEvent) => {\n      if (frameRef.current) cancelAnimationFrame(frameRef.current);\n      if (timeout.current) clearTimeout(timeout.current);\n\n      const wasLongPress = pressState.isLongPressed;\n\n      if (!wasLongPress) {\n        if (pressState.progress < 1) {\n          onLongPressCanceled?.();\n        }\n        onPress?.();\n      }\n\n      setPressState({ isPressed: false, isLongPressed: false, progress: 0 });\n    },\n    [onLongPressCanceled, onPress, pressState.isLongPressed, pressState.progress]\n  );\n\n  /**\n   * Handles cancellation of a press interaction\n   * (e.g., pointer leave or touch cancel events)\n   */\n  const cancel = useCallback(\n    (event: React.TouchEvent | React.MouseEvent) => {\n      if (frameRef.current) cancelAnimationFrame(frameRef.current);\n      if (timeout.current) clearTimeout(timeout.current);\n\n      if (pressState.isPressed && !pressState.isLongPressed) {\n        onLongPressCanceled?.();\n      }\n\n      setPressState({ isPressed: false, isLongPressed: false, progress: 0 });\n    },\n    [onLongPressCanceled, pressState.isPressed, pressState.isLongPressed]\n  );\n\n  return {\n    /** Event handlers to attach to the target element */\n    handlers: {\n      onMouseDown: start,\n      onMouseUp: end,\n      onMouseLeave: cancel,\n      onTouchStart: start,\n      onTouchEnd: end,\n      onTouchCancel: cancel,\n    },\n    /** Current state of the press interaction */\n    state: {\n      isPressed: pressState.isPressed,\n      isLongPressed: pressState.isLongPressed,\n      progress: pressState.progress,\n    },\n  };\n}\n"]}