diff --git a/dev/examples/mouse-events.tsx b/dev/examples/mouse-events.tsx
new file mode 100644
index 0000000000..0934d42291
--- /dev/null
+++ b/dev/examples/mouse-events.tsx
@@ -0,0 +1,17 @@
+import * as React from "react"
+import { useRef } from "react"
+import { usePanGesture } from "@framer"
+import { Box } from "../styled"
+export const App = () => {
+ const ref = useRef(null)
+ const [point, setPoint] = React.useState({ x: 0, y: 0 })
+ usePanGesture(
+ {
+ onPan: ({ devicePoint }) => {
+ setPoint(devicePoint)
+ },
+ },
+ ref
+ )
+ return
+}
diff --git a/src/events/event-info.ts b/src/events/event-info.ts
new file mode 100644
index 0000000000..c0c75441de
--- /dev/null
+++ b/src/events/event-info.ts
@@ -0,0 +1,59 @@
+import { EventInfo, Point, EventHandler } from "./types"
+
+interface EventLike {
+ pageX: number
+ pageY: number
+ target: EventTarget | null
+}
+
+const pointForTarget = (event: EventLike, target: HTMLElement | null): Point => {
+ if (!target) {
+ return { x: event.pageX, y: event.pageY }
+ }
+ // Safari
+ if (window.webkitConvertPointFromPageToNode) {
+ let webkitPoint = new WebKitPoint(event.pageX, event.pageY)
+ webkitPoint = window.webkitConvertPointFromPageToNode(target, webkitPoint)
+ return { x: webkitPoint.x, y: webkitPoint.y }
+ }
+ // All other browsers
+ // TODO: This does not work with rotate yet
+ const rect = target.getBoundingClientRect()
+ let scaleX = 1
+ if (target.style.width && target.style.width !== "") {
+ scaleX = parseFloat(target.style.width) / rect.width
+ }
+ let scaleY = 1
+ if (target.style.height && target.style.height !== "") {
+ scaleY = parseFloat(target.style.height) / rect.height
+ }
+ const scale = {
+ x: scaleX,
+ y: scaleY,
+ }
+ const point = {
+ x: scale.x * (event.pageX - rect.left - target.clientLeft + target.scrollLeft),
+ y: scale.y * (event.pageY - rect.top - target.clientTop + target.scrollTop),
+ }
+ return point
+}
+
+const extractEventInfo = (event: EventLike): EventInfo => {
+ const target = event.target instanceof HTMLElement ? event.target : null
+ const point = pointForTarget(event, target)
+ const devicePoint = pointForTarget(event, document.body)
+ return { point, devicePoint }
+}
+
+export const wrapHandler = (handler?: EventHandler): EventListener | undefined => {
+ if (!handler) {
+ return undefined
+ }
+ const listener: EventListener = (event: any, info?: EventInfo) => {
+ if (!info) {
+ info = extractEventInfo(event)
+ }
+ handler(event, info)
+ }
+ return listener
+}
diff --git a/src/events/index.ts b/src/events/index.ts
new file mode 100644
index 0000000000..3300b5d594
--- /dev/null
+++ b/src/events/index.ts
@@ -0,0 +1,2 @@
+export { useMouseEvents, useTouchEvents, usePointerEvents } from "./use-pointer-events"
+export { Point, EventInfo } from "./types"
diff --git a/src/events/types.ts b/src/events/types.ts
new file mode 100644
index 0000000000..6555e893d8
--- /dev/null
+++ b/src/events/types.ts
@@ -0,0 +1,11 @@
+export interface Point {
+ x: number
+ y: number
+}
+
+export interface EventInfo {
+ point: Point
+ devicePoint: Point
+}
+
+export type EventHandler = (event: Event, info: EventInfo) => void
diff --git a/src/events/use-event.ts b/src/events/use-event.ts
new file mode 100644
index 0000000000..455c82b54f
--- /dev/null
+++ b/src/events/use-event.ts
@@ -0,0 +1,39 @@
+import { RefObject, useEffect } from "react"
+
+export const eventListener = (
+ target: EventTarget,
+ name: string,
+ handler: EventListenerOrEventListenerObject,
+ options?: AddEventListenerOptions
+): [() => void, () => void] => {
+ const startListening = () => {
+ target.addEventListener(name, handler, options)
+ }
+ const stopListening = () => {
+ target.removeEventListener(name, handler, options)
+ }
+ return [startListening, stopListening]
+}
+
+export const useEvent = (
+ type: string,
+ ref: RefObject | EventTarget,
+ handler?: EventListener,
+ options?: AddEventListenerOptions
+): [() => void, () => void] | undefined => {
+ if (!handler) {
+ return
+ }
+ if (ref instanceof EventTarget) {
+ return eventListener(ref, type, handler, options)
+ }
+ useEffect(() => {
+ if (!handler || !ref.current) {
+ return
+ }
+ const [start, stop] = eventListener(ref.current, type, handler, options)
+ start()
+ return stop
+ })
+ return
+}
diff --git a/src/events/use-pointer-events.ts b/src/events/use-pointer-events.ts
new file mode 100644
index 0000000000..e7fe30c5ca
--- /dev/null
+++ b/src/events/use-pointer-events.ts
@@ -0,0 +1,86 @@
+import { RefObject } from "react"
+import { useEvent } from "./use-event"
+import { wrapHandler } from "./event-info"
+import { EventHandler } from "./types"
+
+const mergeUseEventResults = (
+ ...values: ([() => void, () => void] | undefined)[]
+): [() => void, () => void] | undefined => {
+ if (values.every(v => v === undefined)) {
+ return
+ }
+ const start = () => {
+ for (const value of values) {
+ if (value) {
+ value[0]()
+ }
+ }
+ }
+ const stop = () => {
+ for (const value of values) {
+ if (value) {
+ value[1]()
+ }
+ }
+ }
+ return [start, stop]
+}
+
+export const useMouseEvents = (
+ { onMouseDown, onMouseMove, onMouseUp }: { [key: string]: EventHandler },
+ ref: RefObject | EventTarget,
+ options?: AddEventListenerOptions
+): [() => void, () => void] | undefined => {
+ const down = useEvent("mousedown", ref, wrapHandler(onMouseDown), options)
+ const move = useEvent("mousemove", ref, wrapHandler(onMouseMove), options)
+ const up = useEvent("mouseup", ref, wrapHandler(onMouseUp), options)
+ return mergeUseEventResults(down, move, up)
+}
+
+export const useTouchEvents = (
+ { onTouchStart, onTouchMove, onTouchEnd }: { [key: string]: EventHandler },
+ ref: RefObject | EventTarget,
+ options?: AddEventListenerOptions
+) => {
+ const down = useEvent("touchstart", ref, wrapHandler(onTouchStart), options)
+ const move = useEvent("touchmove", ref, wrapHandler(onTouchMove), options)
+ const up = useEvent("touchend", ref, wrapHandler(onTouchEnd), options)
+ return mergeUseEventResults(down, move, up)
+}
+
+export const useNativePointerEvents = (
+ { onPointerDown, onPointerMove, onPointerUp }: { [key: string]: EventHandler },
+ ref: RefObject | EventTarget,
+ options?: AddEventListenerOptions
+) => {
+ const down = useEvent("pointerdown", ref, wrapHandler(onPointerDown), options)
+ const move = useEvent("pointermove", ref, wrapHandler(onPointerMove), options)
+ const up = useEvent("pointerup", ref, wrapHandler(onPointerUp), options)
+ return mergeUseEventResults(down, move, up)
+}
+
+const supportsPointerEvents =
+ window.onpointerdown === null && window.onpointermove === null && window.onpointerup === null
+const supportsTouchEvents = window.ontouchstart === null && window.ontouchmove === null && window.ontouchend === null
+const supportsMouseEvents = window.onmousedown === null && window.onmousemove === null && window.onmouseup === null
+
+export const usePointerEvents = (
+ { onPointerDown, onPointerMove, onPointerUp }: { [key: string]: EventHandler },
+ ref: RefObject | EventTarget,
+ options?: AddEventListenerOptions
+) => {
+ let mouseEvents = {},
+ touchEvents = {},
+ pointerEvents = {}
+ if (supportsPointerEvents) {
+ pointerEvents = { onPointerDown, onPointerMove, onPointerUp }
+ } else if (supportsTouchEvents) {
+ touchEvents = { onTouchStart: onPointerDown, onTouchMove: onPointerMove, onTouchEnd: onPointerUp }
+ } else if (supportsMouseEvents) {
+ mouseEvents = { onMouseDown: onPointerDown, onMouseMove: onPointerMove, onMouseUp: onPointerUp }
+ }
+ const pointer = useNativePointerEvents(pointerEvents, ref, options)
+ const touch = useTouchEvents(touchEvents, ref, options)
+ const mouse = useMouseEvents(mouseEvents, ref, options)
+ return mergeUseEventResults(pointer, touch, mouse) || []
+}
diff --git a/src/gestures/index.ts b/src/gestures/index.ts
new file mode 100644
index 0000000000..2d26e224fc
--- /dev/null
+++ b/src/gestures/index.ts
@@ -0,0 +1 @@
+export { usePanGesture } from "./use-pan-gesture"
diff --git a/src/gestures/use-pan-gesture.ts b/src/gestures/use-pan-gesture.ts
new file mode 100644
index 0000000000..335a85bf07
--- /dev/null
+++ b/src/gestures/use-pan-gesture.ts
@@ -0,0 +1,88 @@
+import { RefObject, useMemo } from "react"
+import { EventInfo, usePointerEvents, Point } from "../events"
+
+export const usePanGesture = (
+ {
+ onPan,
+ onPanStart,
+ onPanEnd,
+ }: { [key: string]: (info: { point: Point; devicePoint: Point; delta: Point }, event: Event) => void },
+ ref: RefObject
+) => {
+ let session: null | any = null
+ const onPointerMove = useMemo(
+ () => {
+ return (event: Event, { point, devicePoint }: EventInfo) => {
+ if (!session) {
+ // tslint:disable-next-line:no-console
+ console.error("Pointer move without started session")
+ return
+ }
+
+ const delta = {
+ x: devicePoint.x - session.lastDevicePoint.x,
+ y: devicePoint.y - session.lastDevicePoint.y,
+ }
+ if (Math.abs(delta.x) > 0 || Math.abs(delta.y) > 0) {
+ if (session.startEvent) {
+ if (onPan) {
+ onPan({ point, devicePoint, delta }, event)
+ }
+ } else {
+ if (onPanStart) {
+ onPanStart({ point, devicePoint, delta }, event)
+ }
+ session.startEvent = event
+ }
+ }
+ session.lastDevicePoint = devicePoint
+ }
+ },
+ [onPan, onPanStart]
+ )
+ const onPointerUp = useMemo(
+ () => {
+ return (event: Event, { point, devicePoint }: EventInfo) => {
+ if (!session) {
+ // tslint:disable-next-line:no-console
+ console.error("Pointer end without started session")
+ return
+ }
+ const delta = {
+ x: devicePoint.x - session.lastDevicePoint.x,
+ y: devicePoint.y - session.lastDevicePoint.y,
+ }
+ stopPointerMove()
+ stopPointerUp()
+ if (onPanEnd) {
+ onPanEnd({ point, devicePoint, delta }, event)
+ }
+ session = null
+ }
+ },
+ [onPanEnd, onPointerMove]
+ )
+
+ const [startPointerUp, stopPointerUp] = usePointerEvents({ onPointerUp }, window)
+ const [startPointerMove, stopPointerMove] = usePointerEvents({ onPointerMove }, window, { capture: true })
+ const onPointerDown = useMemo(
+ () => {
+ return (event: Event, { devicePoint }: EventInfo) => {
+ session = {
+ target: event.target,
+ lastDevicePoint: devicePoint,
+ }
+ startPointerMove()
+ startPointerUp()
+ }
+ },
+ [onPointerUp, onPointerMove]
+ )
+
+ usePointerEvents({ onPointerDown }, ref)
+
+ // TODO
+ const handlers = {}
+
+ return handlers
+}
diff --git a/src/index.ts b/src/index.ts
index e8e8e43bfc..62c5a01b7e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -4,3 +4,6 @@ import { useTransform } from "./hooks/use-transform"
import { usePose } from "./hooks/use-pose"
export { motion, useMotionValue, useTransform, usePose }
+
+export { useMouseEvents, useTouchEvents, usePointerEvents } from "./events"
+export { usePanGesture } from "./gestures"