@react-three/fiber 7.0.19 → 7.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,538 @@
1
+ import { c as createEvents, q as buildGraph, r as is, a as createLoop, b as createRenderer, e as createStore, f as context, g as dispose, i as isRenderer } from '../../dist/hooks-8fbc4995.esm.js';
2
+ export { t as ReactThreeFiber, k as addAfterEffect, j as addEffect, l as addTail, f as context, g as dispose, h as extend, n as useFrame, o as useGraph, u as useStore, m as useThree } from '../../dist/hooks-8fbc4995.esm.js';
3
+ import * as THREE from 'three';
4
+ import * as React from 'react';
5
+ import { DefaultEventPriority, ContinuousEventPriority, DiscreteEventPriority, ConcurrentRoot } from 'react-reconciler/constants';
6
+ import Pressability from 'react-native/Libraries/Pressability/Pressability';
7
+ import { View, StyleSheet, PixelRatio } from 'react-native';
8
+ import { Asset } from 'expo-asset';
9
+ import { readAsStringAsync } from 'expo-file-system';
10
+ import { decode } from 'base64-arraybuffer';
11
+ import { suspend, preload, clear } from 'suspend-react';
12
+ import _extends from '@babel/runtime/helpers/esm/extends';
13
+ import { GLView } from 'expo-gl';
14
+ import pick from 'lodash-es/pick';
15
+ import omit from 'lodash-es/omit';
16
+ import 'react-reconciler';
17
+ import 'zustand';
18
+
19
+ // @ts-ignore
20
+ const EVENTS = {
21
+ PRESS: 'onPress',
22
+ PRESSIN: 'onPressIn',
23
+ PRESSOUT: 'onPressOut',
24
+ LONGPRESS: 'onLongPress',
25
+ HOVERIN: 'onHoverIn',
26
+ HOVEROUT: 'onHoverOut',
27
+ PRESSMOVE: 'onPressMove'
28
+ };
29
+ const DOM_EVENTS = {
30
+ [EVENTS.PRESS]: 'onClick',
31
+ [EVENTS.PRESSIN]: 'onPointerDown',
32
+ [EVENTS.PRESSOUT]: 'onPointerUp',
33
+ [EVENTS.LONGPRESS]: 'onDoubleClick',
34
+ [EVENTS.HOVERIN]: 'onPointerOver',
35
+ [EVENTS.HOVEROUT]: 'onPointerOut',
36
+ [EVENTS.PRESSMOVE]: 'onPointerMove'
37
+ }; // https://github.com/facebook/react/tree/main/packages/react-reconciler#getcurrenteventpriority
38
+ // Gives React a clue as to how import the current interaction is
39
+
40
+ function getEventPriority() {
41
+ var _window, _window$event;
42
+
43
+ let name = (_window = window) == null ? void 0 : (_window$event = _window.event) == null ? void 0 : _window$event.type;
44
+
45
+ switch (name) {
46
+ case EVENTS.PRESS:
47
+ case EVENTS.PRESSIN:
48
+ case EVENTS.PRESSOUT:
49
+ case EVENTS.LONGPRESS:
50
+ return DiscreteEventPriority;
51
+
52
+ case EVENTS.HOVERIN:
53
+ case EVENTS.HOVEROUT:
54
+ case EVENTS.PRESSMOVE:
55
+ return ContinuousEventPriority;
56
+
57
+ default:
58
+ return DefaultEventPriority;
59
+ }
60
+ }
61
+ function createTouchEvents(store) {
62
+ const {
63
+ handlePointer
64
+ } = createEvents(store);
65
+
66
+ const handleTouch = (event, name) => {
67
+ event.persist() // Apply offset
68
+ ;
69
+ event.nativeEvent.offsetX = event.nativeEvent.pageX;
70
+ event.nativeEvent.offsetY = event.nativeEvent.pageY; // Emulate DOM event
71
+
72
+ const callback = handlePointer(DOM_EVENTS[name]);
73
+ return callback(event.nativeEvent);
74
+ };
75
+
76
+ return {
77
+ connected: false,
78
+ handlers: Object.values(EVENTS).reduce((acc, name) => ({ ...acc,
79
+ [name]: event => handleTouch(event, name)
80
+ }), {}),
81
+ connect: () => {
82
+ const {
83
+ set,
84
+ events
85
+ } = store.getState();
86
+ events.disconnect == null ? void 0 : events.disconnect();
87
+ const manager = new Pressability(events == null ? void 0 : events.handlers);
88
+ set(state => ({
89
+ events: { ...state.events,
90
+ connected: manager
91
+ }
92
+ }));
93
+ },
94
+ disconnect: () => {
95
+ const {
96
+ set,
97
+ events
98
+ } = store.getState();
99
+
100
+ if (events.connected) {
101
+ events.connected.reset();
102
+ set(state => ({
103
+ events: { ...state.events,
104
+ connected: false
105
+ }
106
+ }));
107
+ }
108
+ }
109
+ };
110
+ }
111
+
112
+ const CANVAS_PROPS = ['gl', 'events', 'size', 'shadows', 'linear', 'flat', 'orthographic', 'frameloop', // 'dpr',
113
+ 'performance', 'clock', 'raycaster', 'camera', 'onPointerMissed', 'onCreated']; // React currently throws a warning when using useLayoutEffect on the server.
114
+ // To get around it, we can conditionally useEffect on the server (no-op) and
115
+ // useLayoutEffect in the browser.
116
+
117
+ const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
118
+
119
+ function Block({
120
+ set
121
+ }) {
122
+ useIsomorphicLayoutEffect(() => {
123
+ set(new Promise(() => null));
124
+ return () => set(false);
125
+ }, []);
126
+ return null;
127
+ }
128
+
129
+ class ErrorBoundary extends React.Component {
130
+ constructor(...args) {
131
+ super(...args);
132
+ this.state = {
133
+ error: false
134
+ };
135
+ }
136
+
137
+ componentDidCatch(error) {
138
+ this.props.set(error);
139
+ }
140
+
141
+ render() {
142
+ return this.state.error ? null : this.props.children;
143
+ }
144
+
145
+ }
146
+
147
+ ErrorBoundary.getDerivedStateFromError = () => ({
148
+ error: true
149
+ });
150
+
151
+ const Canvas = /*#__PURE__*/React.forwardRef(({
152
+ children,
153
+ fallback,
154
+ style,
155
+ events,
156
+ nativeRef_EXPERIMENTAL,
157
+ onContextCreate,
158
+ ...props
159
+ }, forwardedRef) => {
160
+ const canvasProps = pick(props, CANVAS_PROPS);
161
+ const viewProps = omit(props, CANVAS_PROPS);
162
+ const [context, setContext] = React.useState(null);
163
+ const [{
164
+ width,
165
+ height
166
+ }, setSize] = React.useState({
167
+ width: 0,
168
+ height: 0
169
+ });
170
+ const [bind, setBind] = React.useState();
171
+ const [block, setBlock] = React.useState(false);
172
+ const [error, setError] = React.useState(false); // Suspend this component if block is a promise (2nd run)
173
+
174
+ if (block) throw block; // Throw exception outwards if anything within canvas throws
175
+
176
+ if (error) throw error;
177
+ const onLayout = React.useCallback(e => {
178
+ const {
179
+ width,
180
+ height
181
+ } = e.nativeEvent.layout;
182
+ setSize({
183
+ width,
184
+ height
185
+ });
186
+ }, []); // Execute JSX in the reconciler as a layout-effect
187
+
188
+ useIsomorphicLayoutEffect(() => {
189
+ if (width > 0 && height > 0 && context) {
190
+ const store = render( /*#__PURE__*/React.createElement(ErrorBoundary, {
191
+ set: setError
192
+ }, /*#__PURE__*/React.createElement(React.Suspense, {
193
+ fallback: /*#__PURE__*/React.createElement(Block, {
194
+ set: setBlock
195
+ })
196
+ }, children)), context, { ...canvasProps,
197
+ size: {
198
+ width,
199
+ height
200
+ },
201
+ events: events || createTouchEvents
202
+ });
203
+ const state = store.getState();
204
+ setBind(state.events.connected.getEventHandlers());
205
+ }
206
+ }, [width, height, children, context, canvasProps]);
207
+ useIsomorphicLayoutEffect(() => {
208
+ return () => {
209
+ if (context) unmountComponentAtNode(context);
210
+ };
211
+ }, []);
212
+ return /*#__PURE__*/React.createElement(View, _extends({}, viewProps, {
213
+ ref: forwardedRef,
214
+ onLayout: onLayout,
215
+ style: {
216
+ flex: 1,
217
+ ...style
218
+ }
219
+ }, bind), width > 0 && /*#__PURE__*/React.createElement(GLView, {
220
+ nativeRef_EXPERIMENTAL: ref => {
221
+ if (nativeRef_EXPERIMENTAL && !nativeRef_EXPERIMENTAL.current) {
222
+ nativeRef_EXPERIMENTAL.current = ref;
223
+ }
224
+ },
225
+ onContextCreate: async gl => {
226
+ await (onContextCreate == null ? void 0 : onContextCreate(gl));
227
+ setContext(gl);
228
+ },
229
+ style: StyleSheet.absoluteFill
230
+ }));
231
+ });
232
+
233
+ /**
234
+ * Generates an asset based on input type.
235
+ */
236
+
237
+ const getAsset = input => {
238
+ if (input instanceof Asset) return input;
239
+
240
+ switch (typeof input) {
241
+ case 'string':
242
+ return Asset.fromURI(input);
243
+
244
+ case 'number':
245
+ return Asset.fromModule(input);
246
+
247
+ default:
248
+ throw 'Invalid asset! Must be a URI or module.';
249
+ }
250
+ };
251
+ /**
252
+ * Downloads from a local URI and decodes into an ArrayBuffer.
253
+ */
254
+
255
+
256
+ const toBuffer = async localUri => readAsStringAsync(localUri, {
257
+ encoding: 'base64'
258
+ }).then(decode);
259
+
260
+ function loadingFn(extensions, onProgress) {
261
+ return function (Proto, ...input) {
262
+ // Construct new loader and run extensions
263
+ const loader = new Proto();
264
+ if (extensions) extensions(loader); // Go through the urls and load them
265
+
266
+ return Promise.all(input.map(entry => new Promise(async (res, reject) => {
267
+ var _parse, _ref;
268
+
269
+ // Construct URL
270
+ const url = typeof entry === 'string' ? loader.path + entry : entry; // There's no Image in native, so we create & pass a data texture instead
271
+
272
+ if (loader instanceof THREE.TextureLoader) {
273
+ const asset = await getAsset(url).downloadAsync();
274
+ const texture = new THREE.Texture();
275
+ texture.isDataTexture = true;
276
+ texture.image = {
277
+ data: asset,
278
+ width: asset.width,
279
+ height: asset.height
280
+ };
281
+ texture.needsUpdate = true;
282
+ return res(texture);
283
+ } // Do similar for CubeTextures
284
+
285
+
286
+ if (loader instanceof THREE.CubeTextureLoader) {
287
+ const texture = new THREE.CubeTexture();
288
+ texture.isDataTexture = true;
289
+ texture.images = await Promise.all(url.map(async src => {
290
+ const asset = await getAsset(src).downloadAsync();
291
+ return {
292
+ data: asset,
293
+ width: asset.width,
294
+ height: asset.height
295
+ };
296
+ }));
297
+ texture.needsUpdate = true;
298
+ return res(texture);
299
+ } // If asset is external and not an Image, load it
300
+
301
+
302
+ if (url.startsWith != null && url.startsWith('http') && Proto.prototype.hasOwnProperty('load')) {
303
+ return loader.load(entry, data => {
304
+ if (data.scene) Object.assign(data, buildGraph(data.scene));
305
+ res(data);
306
+ }, onProgress, error => reject(`Could not load ${url}: ${error.message}`));
307
+ } // Otherwise, create a localUri and a file buffer
308
+
309
+
310
+ const {
311
+ localUri
312
+ } = await getAsset(url).downloadAsync();
313
+ const arrayBuffer = await toBuffer(localUri); // Parse it
314
+
315
+ const parsed = (_parse = (_ref = loader).parse) == null ? void 0 : _parse.call(_ref, arrayBuffer, undefined, data => {
316
+ if (data.scene) Object.assign(data, buildGraph(data.scene));
317
+ res(data);
318
+ }, error => reject(`Could not load ${url}: ${error.message}`)); // Respect synchronous parsers which don't have callbacks
319
+
320
+ if (parsed) return res(parsed);
321
+ })));
322
+ };
323
+ }
324
+
325
+ function useLoader(Proto, input, extensions, onProgress) {
326
+ // Use suspense to load async assets
327
+ const keys = Array.isArray(input) ? input : [input];
328
+ const results = suspend(loadingFn(extensions, onProgress), [Proto, ...keys], {
329
+ equal: is.equ
330
+ }); // Return the object/s
331
+
332
+ return Array.isArray(input) ? results : results[0];
333
+ }
334
+
335
+ useLoader.preload = function (Proto, input, extensions) {
336
+ const keys = Array.isArray(input) ? input : [input];
337
+ return preload(loadingFn(extensions), [Proto, ...keys]);
338
+ };
339
+
340
+ useLoader.clear = function (Proto, input) {
341
+ const keys = Array.isArray(input) ? input : [input];
342
+ return clear([Proto, ...keys]);
343
+ };
344
+
345
+ const roots = new Map();
346
+ const {
347
+ invalidate,
348
+ advance
349
+ } = createLoop(roots);
350
+ const {
351
+ reconciler,
352
+ applyProps
353
+ } = createRenderer(roots, getEventPriority);
354
+
355
+ const createRendererInstance = (gl, context) => {
356
+ // Create canvas shim
357
+ const canvas = {
358
+ width: context.drawingBufferWidth,
359
+ height: context.drawingBufferHeight,
360
+ style: {},
361
+ addEventListener: () => {},
362
+ removeEventListener: () => {},
363
+ clientHeight: context.drawingBufferHeight
364
+ }; // If a renderer is specified, return it
365
+
366
+ const customRenderer = typeof gl === 'function' ? gl(canvas, context) : gl;
367
+ if (isRenderer(customRenderer)) return customRenderer; // Create renderer and pass our canvas and its context
368
+
369
+ const renderer = new THREE.WebGLRenderer({
370
+ powerPreference: 'high-performance',
371
+ antialias: true,
372
+ alpha: true,
373
+ ...gl,
374
+ canvas,
375
+ context
376
+ }); // Set color management
377
+
378
+ renderer.outputEncoding = THREE.sRGBEncoding;
379
+ renderer.toneMapping = THREE.ACESFilmicToneMapping; // Set GL props
380
+
381
+ if (gl) applyProps(renderer, gl); // Bind render to RN bridge
382
+
383
+ if (context.endFrameEXP) {
384
+ const renderFrame = renderer.render.bind(renderer);
385
+
386
+ renderer.render = (scene, camera) => {
387
+ renderFrame(scene, camera);
388
+ context.endFrameEXP();
389
+ };
390
+ }
391
+
392
+ return renderer;
393
+ };
394
+
395
+ function createRoot(context, config) {
396
+ return {
397
+ render: element => render(element, context, config),
398
+ unmount: () => unmountComponentAtNode(context)
399
+ };
400
+ }
401
+
402
+ function render(element, context, {
403
+ gl,
404
+ size = {
405
+ width: 0,
406
+ height: 0
407
+ },
408
+ events,
409
+ onCreated,
410
+ ...props
411
+ } = {}) {
412
+ var _store;
413
+
414
+ let root = roots.get(context);
415
+ let fiber = root == null ? void 0 : root.fiber;
416
+ let store = root == null ? void 0 : root.store;
417
+ let state = (_store = store) == null ? void 0 : _store.getState();
418
+
419
+ if (fiber && state) {
420
+ // When a root was found, see if any fundamental props must be changed or exchanged
421
+ // Check size
422
+ if (state.size.width !== size.width || state.size.height !== size.height) state.setSize(size.width, size.height); // For some props we want to reset the entire root
423
+ // Changes to the color-space
424
+
425
+ const linearChanged = props.linear !== state.internal.lastProps.linear;
426
+
427
+ if (linearChanged) {
428
+ unmountComponentAtNode(context);
429
+ fiber = undefined;
430
+ }
431
+ }
432
+
433
+ if (!fiber) {
434
+ // If no root has been found, make one
435
+ // Create gl
436
+ const glRenderer = createRendererInstance(gl, context); // Create store
437
+
438
+ store = createStore(applyProps, invalidate, advance, {
439
+ gl: glRenderer,
440
+ size,
441
+ ...props,
442
+ // expo-gl can only render at native dpr/resolution
443
+ // https://github.com/expo/expo-three/issues/39
444
+ dpr: PixelRatio.get()
445
+ });
446
+ const state = store.getState(); // Create renderer
447
+
448
+ fiber = reconciler.createContainer(store, ConcurrentRoot, false, null); // Map it
449
+
450
+ roots.set(context, {
451
+ fiber,
452
+ store
453
+ }); // Store event manager internally and connect it
454
+
455
+ if (events) {
456
+ var _state$get$events$con, _state$get$events;
457
+
458
+ state.set({
459
+ events: events(store)
460
+ });
461
+ (_state$get$events$con = (_state$get$events = state.get().events).connect) == null ? void 0 : _state$get$events$con.call(_state$get$events, context);
462
+ }
463
+ }
464
+
465
+ if (store && fiber) {
466
+ reconciler.updateContainer( /*#__PURE__*/React.createElement(Provider, {
467
+ store: store,
468
+ element: element,
469
+ onCreated: onCreated
470
+ }), fiber, null, () => undefined);
471
+ return store;
472
+ } else {
473
+ throw 'Error creating root!';
474
+ }
475
+ }
476
+
477
+ function Provider({
478
+ store,
479
+ element,
480
+ onCreated
481
+ }) {
482
+ React.useEffect(() => {
483
+ const state = store.getState(); // Flag the canvas active, rendering will now begin
484
+
485
+ state.set(state => ({
486
+ internal: { ...state.internal,
487
+ active: true
488
+ }
489
+ })); // Notifiy that init is completed, the scene graph exists, but nothing has yet rendered
490
+
491
+ if (onCreated) onCreated(state); // eslint-disable-next-line react-hooks/exhaustive-deps
492
+ }, []);
493
+ return /*#__PURE__*/React.createElement(context.Provider, {
494
+ value: store
495
+ }, element);
496
+ }
497
+
498
+ function unmountComponentAtNode(context, callback) {
499
+ const root = roots.get(context);
500
+ const fiber = root == null ? void 0 : root.fiber;
501
+
502
+ if (fiber) {
503
+ const state = root == null ? void 0 : root.store.getState();
504
+ if (state) state.internal.active = false;
505
+ reconciler.updateContainer(null, fiber, null, () => {
506
+ if (state) {
507
+ setTimeout(() => {
508
+ try {
509
+ var _state$gl, _state$gl$renderLists, _state$gl2;
510
+
511
+ state.events.disconnect == null ? void 0 : state.events.disconnect();
512
+ (_state$gl = state.gl) == null ? void 0 : (_state$gl$renderLists = _state$gl.renderLists) == null ? void 0 : _state$gl$renderLists.dispose == null ? void 0 : _state$gl$renderLists.dispose();
513
+ (_state$gl2 = state.gl) == null ? void 0 : _state$gl2.forceContextLoss == null ? void 0 : _state$gl2.forceContextLoss();
514
+ dispose(state);
515
+ roots.delete(context);
516
+ if (callback) callback(context);
517
+ } catch (e) {
518
+ /* ... */
519
+ }
520
+ }, 500);
521
+ }
522
+ });
523
+ }
524
+ }
525
+
526
+ const act = reconciler.act;
527
+
528
+ function createPortal(children, container) {
529
+ return reconciler.createPortal(children, container, null, null);
530
+ }
531
+
532
+ reconciler.injectIntoDevTools({
533
+ bundleType: process.env.NODE_ENV === 'production' ? 0 : 1,
534
+ rendererPackageName: '@react-three/fiber',
535
+ version: '18.0.0'
536
+ });
537
+
538
+ export { Canvas, roots as _roots, act, advance, applyProps, createPortal, createRoot, createTouchEvents as events, invalidate, reconciler, render, unmountComponentAtNode, useLoader };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-three/fiber",
3
- "version": "7.0.19",
3
+ "version": "7.0.23",
4
4
  "description": "A React renderer for Threejs",
5
5
  "keywords": [
6
6
  "react",
@@ -12,7 +12,8 @@
12
12
  "author": "Paul Henschel (https://github.com/drcmda)",
13
13
  "license": "MIT",
14
14
  "maintainers": [
15
- "Josh Ellis (https://github.com/joshuaellis)"
15
+ "Josh Ellis (https://github.com/joshuaellis)",
16
+ "Cody Bennett (https://github.com/codyjasonbennett)"
16
17
  ],
17
18
  "bugs": {
18
19
  "url": "https://github.com/pmndrs/react-three-fiber/issues"
@@ -44,7 +45,7 @@
44
45
  "react-merge-refs": "^1.1.0",
45
46
  "react-reconciler": "^0.26.2",
46
47
  "react-three-fiber": "0.0.0-deprecated",
47
- "react-use-measure": "^2.0.4",
48
+ "react-use-measure": "^2.1.1",
48
49
  "resize-observer-polyfill": "^1.5.1",
49
50
  "scheduler": "^0.20.2",
50
51
  "use-asset": "^1.0.4",
package/readme.md CHANGED
@@ -156,7 +156,7 @@ ReactDOM.render(
156
156
  You need to be versed in both React and Threejs before rushing into this. If you are unsure about React consult the official [React docs](https://reactjs.org/docs/getting-started.html), especially [the section about hooks](https://reactjs.org/docs/hooks-reference.html). As for Threejs, make sure you at least glance over the following links:
157
157
 
158
158
  1. Make sure you have a [basic grasp of Threejs](https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene). Keep that site open.
159
- 2. When you know what a scene is, a camera, mesh, geometry, material, fork the [demo above](https://github.com/react-spring/react-three-fiber#what-does-it-look-like).
159
+ 2. When you know what a scene is, a camera, mesh, geometry, material, fork the [demo above](https://github.com/pmndrs/react-three-fiber#what-does-it-look-like).
160
160
  3. [Look up](https://threejs.org/docs/index.html#api/en/objects/Mesh) the JSX elements that you see (mesh, ambientLight, etc), _all_ threejs exports are native to three-fiber.
161
161
  4. Try changing some values, scroll though our [Api](/markdown/api.md) to see what the various settings and hooks do.
162
162
 
@@ -171,15 +171,15 @@ Some reading material:
171
171
 
172
172
  # Ecosystem
173
173
 
174
- - [`@react-three/gltfjsx`](https://github.com/react-spring/gltfjsx) – turns GLTFs into JSX components
175
- - [`@react-three/drei`](https://github.com/react-spring/drei) – useful helpers for react-three-fiber
176
- - [`@react-three/postprocessing`](https://github.com/react-spring/react-postprocessing) – post-processing effects
177
- - [`@react-three/flex`](https://github.com/react-spring/react-three-flex) – flexbox for react-three-fiber
178
- - [`@react-three/xr`](https://github.com/react-spring/react-xr) – VR/AR controllers and events
179
- - [`@react-three/cannon`](https://github.com/react-spring/use-cannon) – physics based hooks
180
- - [`zustand`](https://github.com/react-spring/zustand) – state management
181
- - [`react-spring`](https://github.com/react-spring/react-spring) – a spring-physics-based animation library
182
- - [`react-use-gesture`](https://github.com/react-spring/react-use-gesture) – mouse/touch gestures
174
+ - [`@react-three/gltfjsx`](https://github.com/pmndrs/gltfjsx) – turns GLTFs into JSX components
175
+ - [`@react-three/drei`](https://github.com/pmndrs/drei) – useful helpers for react-three-fiber
176
+ - [`@react-three/postprocessing`](https://github.com/pmndrs/react-postprocessing) – post-processing effects
177
+ - [`@react-three/flex`](https://github.com/pmndrs/react-three-flex) – flexbox for react-three-fiber
178
+ - [`@react-three/xr`](https://github.com/pmndrs/react-xr) – VR/AR controllers and events
179
+ - [`@react-three/cannon`](https://github.com/pmndrs/use-cannon) – physics based hooks
180
+ - [`zustand`](https://github.com/pmndrs/zustand) – state management
181
+ - [`react-spring`](https://github.com/pmndrs/react-spring) – a spring-physics-based animation library
182
+ - [`react-use-gesture`](https://github.com/pmndrs/react-use-gesture) – mouse/touch gestures
183
183
 
184
184
  # How to contribute
185
185