diff --git a/src/hooks/useSwipe/useSwipe.demo.tsx b/src/hooks/useSwipe/useSwipe.demo.tsx
new file mode 100644
index 00000000..53d84eb8
--- /dev/null
+++ b/src/hooks/useSwipe/useSwipe.demo.tsx
@@ -0,0 +1,88 @@
+import { useRef, useState } from 'react';
+
+import { useSwipe } from './useSwipe';
+
+const Demo = () => {
+ const ref = useRef(null);
+ const [offset, setOffset] = useState(0);
+ const [isSwiped, setIsSwiped] = useState(false);
+
+ const { isSwiping, percent } = useSwipe(ref, {
+ threshold: 10,
+ directions: ['right'],
+ onSwiping: ({ percent }) => {
+ setOffset(percent);
+ },
+ onSwiped: ({ percent }) => {
+ if (percent > 80) {
+ setOffset(100);
+ setIsSwiped(true);
+ } else {
+ setOffset(0);
+ }
+ }
+ });
+
+ const reset = () => {
+ setOffset(0);
+ setIsSwiped(false);
+ };
+
+ return (
+ <>
+
swipe more than 80%
+
+
+ Swipe right
+
+
+
+
+ isSwiping: {isSwiping ? 'swiping' : 'not swiping'}
+
+
+ percent: {percent}%
+
+
+ isSwiped: {isSwiped ? 'swiped' : 'not swiped'}
+
+ >
+ );
+};
+
+export default Demo;
diff --git a/src/hooks/useSwipe/useSwipe.ts b/src/hooks/useSwipe/useSwipe.ts
new file mode 100644
index 00000000..42f1e06e
--- /dev/null
+++ b/src/hooks/useSwipe/useSwipe.ts
@@ -0,0 +1,327 @@
+import React from 'react';
+
+type UseSwipeTarget = React.RefObject | (() => Element) | Element;
+export type UseSwipeDirection = 'up' | 'down' | 'left' | 'right' | 'none';
+export type UseSwipeHandledEvents = React.MouseEvent | TouchEvent | MouseEvent;
+export type UseSwipeCallback = (value: UseSwipeReturn) => void;
+export type UseSwipePosition = { x: number; y: number };
+
+export type UseSwipeReturn = {
+ direction: UseSwipeDirection;
+ isSwiping: boolean;
+ distanceX: number;
+ distanceY: number;
+ percent: number;
+ posStart: UseSwipePosition;
+ posEnd: UseSwipePosition;
+ event: UseSwipeHandledEvents | null;
+};
+
+export type UseSwipeActions = {
+ onSwipeStart?: UseSwipeCallback;
+ onSwiping?: UseSwipeCallback;
+ onSwiped?: UseSwipeCallback;
+ onSwipedLeft?: UseSwipeCallback;
+ onSwipedRight?: UseSwipeCallback;
+ onSwipedUp?: UseSwipeCallback;
+ onSwipedDown?: UseSwipeCallback;
+};
+
+export type UseSwipeOptions = {
+ /** Min distance(px) before a swipe starts. **Default**: `10` */
+ threshold: number;
+ /** Prevents scroll during swipe. **Default**: `false` */
+ preventScrollOnSwipe: boolean;
+ /** Track inputs. **Default**: ['mouse', 'touch'] */
+ track: ['mouse', 'touch'];
+ /** Direction(s) to track. **Default**: `['left', 'right', 'up', 'down']` */
+ directions: UseSwipeDirection[];
+} & UseSwipeActions;
+
+export type UseSwipe = {
+ (target: Target, callback?: UseSwipeCallback): UseSwipeReturn;
+
+ (
+ target: Target,
+ options?: Partial
+ ): UseSwipeReturn;
+
+ (
+ callback: UseSwipeCallback,
+ target?: never
+ ): UseSwipeReturn & { ref: React.RefObject };
+
+ (
+ options: Partial,
+ target?: never
+ ): UseSwipeReturn & { ref: React.RefObject };
+};
+
+const USE_SWIPE_DEFAULT_OPTIONS: UseSwipeOptions = {
+ threshold: 10,
+ preventScrollOnSwipe: false,
+ track: ['mouse', 'touch'],
+ directions: ['left', 'right', 'up', 'down']
+};
+
+const USE_SWIPE_DEFAULT_STATE: UseSwipeReturn = {
+ isSwiping: false,
+ direction: 'none',
+ posStart: { x: 0, y: 0 },
+ posEnd: { x: 0, y: 0 },
+ distanceX: 0,
+ distanceY: 0,
+ percent: 0,
+ event: null
+};
+
+const getElement = (target: UseSwipeTarget) => {
+ if (typeof target === 'function') {
+ return target();
+ }
+
+ if (target instanceof Element) {
+ return target;
+ }
+
+ return target.current;
+};
+
+const getUseSwipeOptions = (options: UseSwipeOptions | undefined) => {
+ return {
+ ...USE_SWIPE_DEFAULT_OPTIONS,
+ ...options
+ };
+};
+
+const getSwipeDirection = (deltaX: number, deltaY: number): UseSwipeDirection => {
+ if (Math.abs(deltaX) > Math.abs(deltaY)) {
+ return deltaX > 0 ? 'left' : 'right';
+ }
+ return deltaY > 0 ? 'up' : 'down';
+};
+
+/**
+ * @name useSwipe
+ * @description - Hook that manages a swipe event
+ *
+ * @overload
+ * @template Target The target element
+ * @param {Target} target The target element to be swiped
+ * @param {() => void} [callback] The callback function to be invoked on swipe end
+ * @returns {UseSwipeReturn} The state of the swipe
+ *
+ * @example
+ * const { isSwiping, direction} = useSwipe(ref, (data) => console.log(data));
+ *
+ * @overload
+ * @template Target The target element
+ * @param {Target} target The target element to be swiped
+ * @param {UseSwipeOptions} options An object containing the swipe options
+ *
+ * @example
+ * const {isSwiping, direction} = useSwipe(ref, {
+ * directions: ['left'],
+ * threshold: 20,
+ * preventScrollOnSwipe: true,
+ * track: ['mouse'],
+ * onSwiped: () => console.log('onSwiped'),
+ * onSwiping: () => console.log('onSwiping'),
+ * onSwipedLeft: () => console.log('onSwipedLeft'),
+ * onSwipedRight: () => console.log('onSwipedRight'),
+ * onSwipedUp: () => console.log('onSwipedUp'),
+ * onSwipedDown: () => console.log('onSwipedDown'),
+ * });
+ *
+ * @overload
+ * @template Target The target element
+ * @param {() => void} [callback] The callback function to be invoked on swipe end
+ * @returns {UseSwipeReturn & { ref: React.RefObject }} The state of the swipe
+ *
+ * @example
+ * const { ref, isSwiping, direction} = useSwipe((data) => console.log(data));
+ *
+ * @overload
+ * @template Target The target element
+ * @param {UseSwipeOptions} options An object containing the swipe options
+ *
+ * @example
+ * const {ref, isSwiping, direction} = useSwipe({
+ * directions: ['left'],
+ * threshold: 20,
+ * preventScrollOnSwipe: true,
+ * track: ['mouse'],
+ * onSwiped: () => console.log('onSwiped'),
+ * onSwiping: () => console.log('onSwiping'),
+ * onSwipedLeft: () => console.log('onSwipedLeft'),
+ * onSwipedRight: () => console.log('onSwipedRight'),
+ * onSwipedUp: () => console.log('onSwipedUp'),
+ * onSwipedDown: () => console.log('onSwipedDown'),
+ * });
+ */
+
+export const useSwipe = ((...params: any[]) => {
+ const target = (
+ params[0] instanceof Function || !('current' in params[0]) ? undefined : params[0]
+ ) as UseSwipeTarget | undefined;
+ const userOptions = (
+ target
+ ? typeof params[1] === 'object'
+ ? params[1]
+ : { onSwiped: params[1] }
+ : typeof params[0] === 'object'
+ ? params[0]
+ : { onSwiped: params[0] }
+ ) as UseSwipeOptions | undefined;
+
+ const options = getUseSwipeOptions(userOptions);
+ const internalRef = React.useRef(null);
+
+ const [value, setValue] = React.useState(USE_SWIPE_DEFAULT_STATE);
+
+ const getSwipePositions = (event: UseSwipeHandledEvents) => {
+ const element = target ? getElement(target) : internalRef.current;
+ if (!element) return { x: 0, y: 0 }; // ?
+
+ const isTouch = 'touches' in event;
+ const { clientX, clientY } = isTouch ? event.touches[0] : event;
+ const boundingRect = element.getBoundingClientRect();
+ const x = Math.round(clientX - boundingRect.left);
+ const y = Math.round(clientY - boundingRect.top);
+ return { x, y };
+ };
+
+ const getPercent = (deltaX: number, deltaY: number): Record => {
+ const element = target ? getElement(target) : internalRef.current;
+ if (!element) return { none: 0, down: 0, left: 0, right: 0, up: 0 }; // ?
+ const { width, height } = element.getBoundingClientRect();
+ return {
+ none: 0,
+ left: Math.min(Math.round((Math.abs(deltaX) * 100) / width), 100),
+ right: Math.min(Math.round((Math.abs(deltaX) * 100) / width), 100),
+ up: Math.min(Math.round((Math.abs(deltaY) * 100) / height), 100),
+ down: Math.min(Math.round((Math.abs(deltaY) * 100) / height), 100)
+ };
+ };
+
+ const onMove = (event: UseSwipeHandledEvents) => {
+ setValue((prevValue) => {
+ if (options.preventScrollOnSwipe) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ const { x, y } = getSwipePositions(event);
+ const distanceX = Math.round(prevValue.posStart.x - x);
+ const distanceY = Math.round(prevValue.posStart.y - y);
+ const absX = Math.abs(distanceX);
+ const absY = Math.abs(distanceY);
+ const isThresholdExceeded = Math.max(absX, absY) >= options.threshold;
+ const isSwiping = prevValue.isSwiping || isThresholdExceeded;
+ const direction = isSwiping ? getSwipeDirection(distanceX, distanceY) : 'none';
+ if (!options.directions.includes(direction)) return prevValue;
+
+ const percent = getPercent(distanceX, distanceY);
+ const newValue: UseSwipeReturn = {
+ ...prevValue,
+ isSwiping,
+ direction,
+ event,
+ distanceX,
+ distanceY,
+ posEnd: { x, y },
+ percent: percent[direction]
+ };
+
+ options?.onSwiping?.(newValue);
+
+ return newValue;
+ });
+ };
+
+ const onFinish = (event: UseSwipeHandledEvents) => {
+ setValue((prevValue) => {
+ const newValue: UseSwipeReturn = {
+ ...prevValue,
+ event,
+ isSwiping: false
+ };
+
+ options?.onSwiped?.(newValue);
+
+ const directionCallbacks = {
+ left: options.onSwipedLeft,
+ right: options.onSwipedRight,
+ up: options.onSwipedUp,
+ down: options.onSwipedDown
+ };
+
+ if (newValue.direction === 'none') return newValue;
+
+ const callback = directionCallbacks[newValue.direction];
+ callback?.(newValue);
+
+ return newValue;
+ });
+
+ document.removeEventListener('mousemove', onMove);
+ document.removeEventListener('touchmove', onMove);
+ };
+
+ const onStart = (event: UseSwipeHandledEvents) => {
+ event.preventDefault(); // prevent text selection
+
+ const isTouch = 'touches' in event;
+ if (options?.track.includes('mouse') && !isTouch) {
+ document.addEventListener('mousemove', onMove);
+ document.addEventListener('mouseup', onFinish, { once: true });
+ }
+ if (options?.track.includes('touch') && isTouch) {
+ document.addEventListener('touchmove', onMove);
+ document.addEventListener('touchend', onFinish, { once: true });
+ }
+
+ const { x, y } = getSwipePositions(event);
+
+ setValue((prevValue) => {
+ const newValue: UseSwipeReturn = {
+ ...prevValue,
+ event,
+ posStart: { x, y },
+ direction: 'none'
+ };
+ options?.onSwipeStart?.(newValue);
+ return newValue;
+ });
+ };
+
+ React.useEffect(() => {
+ const element = target ? getElement(target) : internalRef.current;
+ if (!element) return;
+
+ if (options?.track.includes('mouse')) {
+ // @ts-ignore
+ // element.addEventListener('mousedown', (event) <-- has Event type, not MouseEvent
+ element.addEventListener('mousedown', onStart);
+ }
+
+ if (options?.track.includes('touch')) {
+ // @ts-ignore
+ element.addEventListener('touchstart', onStart);
+ }
+
+ return () => {
+ // @ts-ignore
+ element.removeEventListener('mousedown', onStart);
+ // @ts-ignore
+ element.removeEventListener('touchstart', onStart);
+ document.removeEventListener('mousemove', onMove);
+ document.removeEventListener('touchmove', onMove);
+ document.removeEventListener('mouseup', onFinish);
+ document.removeEventListener('touchend', onFinish);
+ };
+ }, []);
+
+ if (target) return { ...value };
+ return { ...value, ref: internalRef };
+}) as UseSwipe;