react-native-3d-model-carousel 0.4.0

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.
Files changed (36) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +166 -0
  3. package/lib/module/components/ModelCarousel.js +281 -0
  4. package/lib/module/components/ModelCarousel.js.map +1 -0
  5. package/lib/module/components/ModelViewer.js +47 -0
  6. package/lib/module/components/ModelViewer.js.map +1 -0
  7. package/lib/module/hooks/useModelLoader.js +10 -0
  8. package/lib/module/hooks/useModelLoader.js.map +1 -0
  9. package/lib/module/index.js +4 -0
  10. package/lib/module/index.js.map +1 -0
  11. package/lib/module/multiply.js +6 -0
  12. package/lib/module/multiply.js.map +1 -0
  13. package/lib/module/package.json +1 -0
  14. package/lib/module/types/glb.d.js +2 -0
  15. package/lib/module/types/glb.d.js.map +1 -0
  16. package/lib/module/types/react-three-native.d.js +2 -0
  17. package/lib/module/types/react-three-native.d.js.map +1 -0
  18. package/lib/typescript/package.json +1 -0
  19. package/lib/typescript/src/components/ModelCarousel.d.ts +41 -0
  20. package/lib/typescript/src/components/ModelCarousel.d.ts.map +1 -0
  21. package/lib/typescript/src/components/ModelViewer.d.ts +15 -0
  22. package/lib/typescript/src/components/ModelViewer.d.ts.map +1 -0
  23. package/lib/typescript/src/hooks/useModelLoader.d.ts +2 -0
  24. package/lib/typescript/src/hooks/useModelLoader.d.ts.map +1 -0
  25. package/lib/typescript/src/index.d.ts +2 -0
  26. package/lib/typescript/src/index.d.ts.map +1 -0
  27. package/lib/typescript/src/multiply.d.ts +2 -0
  28. package/lib/typescript/src/multiply.d.ts.map +1 -0
  29. package/package.json +162 -0
  30. package/src/components/ModelCarousel.tsx +426 -0
  31. package/src/components/ModelViewer.tsx +60 -0
  32. package/src/hooks/useModelLoader.ts +7 -0
  33. package/src/index.tsx +1 -0
  34. package/src/multiply.tsx +3 -0
  35. package/src/types/glb.d.ts +9 -0
  36. package/src/types/react-three-native.d.ts +21 -0
@@ -0,0 +1,2 @@
1
+ export declare const useModelLoader: (modelPath: unknown) => unknown;
2
+ //# sourceMappingURL=useModelLoader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useModelLoader.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useModelLoader.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,GAAI,WAAW,OAAO,YAIhD,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { default as ModelCarousel } from './components/ModelCarousel';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,4BAA4B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function multiply(a: number, b: number): number;
2
+ //# sourceMappingURL=multiply.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multiply.d.ts","sourceRoot":"","sources":["../../../src/multiply.tsx"],"names":[],"mappings":"AAAA,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAErD"}
package/package.json ADDED
@@ -0,0 +1,162 @@
1
+ {
2
+ "name": "react-native-3d-model-carousel",
3
+ "version": "0.4.0",
4
+ "description": "A React Native library for rendering interactive 3D GLB models in a smooth, customizable carousel powered by Three.js",
5
+ "main": "./lib/module/index.js",
6
+ "types": "./lib/typescript/src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "source": "./src/index.tsx",
10
+ "types": "./lib/typescript/src/index.d.ts",
11
+ "default": "./lib/module/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "lib",
18
+ "android",
19
+ "ios",
20
+ "cpp",
21
+ "*.podspec",
22
+ "react-native.config.js",
23
+ "!ios/build",
24
+ "!android/build",
25
+ "!android/gradle",
26
+ "!android/gradlew",
27
+ "!android/gradlew.bat",
28
+ "!android/local.properties",
29
+ "!**/__tests__",
30
+ "!**/__fixtures__",
31
+ "!**/__mocks__",
32
+ "!**/.*"
33
+ ],
34
+ "scripts": {
35
+ "example": "yarn workspace react-native-3d-model-carousel-example",
36
+ "clean": "del-cli lib",
37
+ "prepare": "bob build",
38
+ "typecheck": "tsc",
39
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
40
+ "test": "jest",
41
+ "release": "release-it --only-version"
42
+ },
43
+ "keywords": [
44
+ "react-native",
45
+ "ios",
46
+ "android"
47
+ ],
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/Tehseem110/react-native-3d-model-carousel.git"
51
+ },
52
+ "author": "Tehseem <tehseem010@gmail.com> (https://github.com/Tehseem110)",
53
+ "license": "MIT",
54
+ "bugs": {
55
+ "url": "https://github.com/Tehseem110/react-native-3d-model-carousel/issues"
56
+ },
57
+ "homepage": "https://github.com/Tehseem110/react-native-3d-model-carousel#readme",
58
+ "publishConfig": {
59
+ "registry": "https://registry.npmjs.org/"
60
+ },
61
+ "devDependencies": {
62
+ "@commitlint/config-conventional": "^20.5.0",
63
+ "@eslint/compat": "^2.0.3",
64
+ "@eslint/eslintrc": "^3.3.5",
65
+ "@eslint/js": "^10.0.1",
66
+ "@jest/globals": "^30.0.0",
67
+ "@react-native/babel-preset": "0.83.0",
68
+ "@react-native/eslint-config": "0.83.0",
69
+ "@release-it/conventional-changelog": "^10.0.6",
70
+ "@types/react": "^19.2.0",
71
+ "commitlint": "^20.5.0",
72
+ "del-cli": "^7.0.0",
73
+ "eslint": "^9.39.4",
74
+ "eslint-config-prettier": "^10.1.8",
75
+ "eslint-plugin-ft-flow": "^3.0.11",
76
+ "eslint-plugin-prettier": "^5.5.5",
77
+ "jest": "^30.3.0",
78
+ "lefthook": "^2.1.4",
79
+ "prettier": "^3.8.1",
80
+ "react": "19.0.0",
81
+ "react-native": "0.79.6",
82
+ "react-native-builder-bob": "^0.41.0",
83
+ "release-it": "^19.2.4",
84
+ "turbo": "^2.8.21",
85
+ "typescript": "^6.0.2"
86
+ },
87
+ "peerDependencies": {
88
+ "react": "*",
89
+ "react-native": "*"
90
+ },
91
+ "workspaces": [
92
+ "example"
93
+ ],
94
+ "packageManager": "yarn@4.11.0",
95
+ "react-native-builder-bob": {
96
+ "source": "src",
97
+ "output": "lib",
98
+ "targets": [
99
+ [
100
+ "module",
101
+ {
102
+ "esm": true
103
+ }
104
+ ],
105
+ [
106
+ "typescript",
107
+ {
108
+ "project": "tsconfig.build.json"
109
+ }
110
+ ]
111
+ ]
112
+ },
113
+ "prettier": {
114
+ "quoteProps": "consistent",
115
+ "singleQuote": true,
116
+ "tabWidth": 2,
117
+ "trailingComma": "es5",
118
+ "useTabs": false
119
+ },
120
+ "jest": {
121
+ "preset": "react-native",
122
+ "modulePathIgnorePatterns": [
123
+ "<rootDir>/example/node_modules",
124
+ "<rootDir>/lib/"
125
+ ]
126
+ },
127
+ "commitlint": {
128
+ "extends": [
129
+ "@commitlint/config-conventional"
130
+ ]
131
+ },
132
+ "release-it": {
133
+ "git": {
134
+ "commitMessage": "chore: release ${version}",
135
+ "tagName": "v${version}"
136
+ },
137
+ "npm": {
138
+ "publish": true
139
+ },
140
+ "github": {
141
+ "release": true
142
+ },
143
+ "plugins": {
144
+ "@release-it/conventional-changelog": {
145
+ "preset": {
146
+ "name": "angular"
147
+ }
148
+ }
149
+ }
150
+ },
151
+ "create-react-native-library": {
152
+ "type": "library",
153
+ "languages": "js",
154
+ "tools": [
155
+ "eslint",
156
+ "jest",
157
+ "lefthook",
158
+ "release-it"
159
+ ],
160
+ "version": "0.60.0"
161
+ }
162
+ }
@@ -0,0 +1,426 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import type { ReactNode } from 'react';
3
+ import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
4
+ import type { DimensionValue, StyleProp, ViewStyle } from 'react-native';
5
+ import { useGLTF } from '@react-three/drei/native';
6
+ import ModelViewer from './ModelViewer';
7
+
8
+ export type ModelCarouselConfiguredItem = {
9
+ path: unknown;
10
+ scale?: number;
11
+ position?: number[];
12
+ cameraPosition?: number[];
13
+ };
14
+
15
+ export type ModelCarouselItem = unknown | ModelCarouselConfiguredItem;
16
+
17
+ export type NavigationButtonRenderProps = {
18
+ onPress: () => void;
19
+ disabled: boolean;
20
+ index: number;
21
+ total: number;
22
+ isAnimating: boolean;
23
+ };
24
+
25
+ type Props = {
26
+ models: ModelCarouselItem[];
27
+
28
+ // layout
29
+ width?: DimensionValue;
30
+ height?: DimensionValue;
31
+ containerStyle?: StyleProp<ViewStyle>;
32
+
33
+ // model props
34
+ scale?: number;
35
+ position?: number[];
36
+
37
+ // camera
38
+ cameraPosition?: number[];
39
+ fov?: number;
40
+
41
+ // controls
42
+ autoRotate?: boolean;
43
+ autoRotateSpeed?: number;
44
+ autoPlay?: boolean;
45
+ autoPlayInterval?: number;
46
+ enableSwipeNavigation?: boolean;
47
+ swipeThreshold?: number;
48
+ showPaginationDots?: boolean;
49
+ showDefaultButtons?: boolean;
50
+
51
+ // transition
52
+ transitionDuration?: number;
53
+ transitionScale?: number;
54
+
55
+ // custom controls
56
+ renderPrevButton?: (props: NavigationButtonRenderProps) => ReactNode;
57
+ renderNextButton?: (props: NavigationButtonRenderProps) => ReactNode;
58
+ };
59
+
60
+ const isConfiguredModel = (
61
+ item: ModelCarouselItem
62
+ ): item is ModelCarouselConfiguredItem => {
63
+ return typeof item === 'object' && item !== null && 'path' in item;
64
+ };
65
+
66
+ const getModelPath = (item: ModelCarouselItem) =>
67
+ isConfiguredModel(item) ? item.path : item;
68
+
69
+ const getResolvedValue = <T,>(
70
+ itemValue: T | undefined,
71
+ fallbackValue: T | undefined
72
+ ) => itemValue ?? fallbackValue;
73
+
74
+ const ModelCarousel = ({
75
+ models,
76
+
77
+ width = '100%',
78
+ height = '100%',
79
+ containerStyle,
80
+
81
+ scale,
82
+ position,
83
+
84
+ cameraPosition,
85
+ fov,
86
+
87
+ autoRotate,
88
+ autoRotateSpeed,
89
+ autoPlay = false,
90
+ autoPlayInterval = 2500,
91
+ enableSwipeNavigation = true,
92
+ swipeThreshold = 36,
93
+ showPaginationDots = true,
94
+ showDefaultButtons = false,
95
+
96
+ transitionDuration = 260,
97
+ transitionScale = 0.96,
98
+ renderPrevButton,
99
+ renderNextButton,
100
+ }: Props) => {
101
+ const [index, setIndex] = useState(0);
102
+ const fade = useRef(new Animated.Value(1)).current;
103
+ const zoom = useRef(new Animated.Value(1)).current;
104
+ const isAnimating = useRef(false);
105
+ const touchStartX = useRef<number | null>(null);
106
+ const touchStartY = useRef<number | null>(null);
107
+
108
+ useEffect(() => {
109
+ if (models.length === 0) {
110
+ setIndex(0);
111
+ return;
112
+ }
113
+
114
+ if (index >= models.length) {
115
+ setIndex(0);
116
+ }
117
+ }, [index, models.length]);
118
+
119
+ useEffect(() => {
120
+ models.forEach((modelItem) => {
121
+ useGLTF.preload(getModelPath(modelItem) as string);
122
+ });
123
+ }, [models]);
124
+
125
+ const runTransition = useCallback(
126
+ (nextIndex: number) => {
127
+ if (isAnimating.current || models.length <= 1) {
128
+ return;
129
+ }
130
+
131
+ isAnimating.current = true;
132
+
133
+ Animated.parallel([
134
+ Animated.timing(fade, {
135
+ toValue: 0,
136
+ duration: Math.round(transitionDuration * 0.45),
137
+ useNativeDriver: true,
138
+ }),
139
+ Animated.timing(zoom, {
140
+ toValue: transitionScale,
141
+ duration: Math.round(transitionDuration * 0.45),
142
+ useNativeDriver: true,
143
+ }),
144
+ ]).start(() => {
145
+ setIndex(nextIndex);
146
+ fade.setValue(0);
147
+ zoom.setValue(transitionScale);
148
+
149
+ Animated.parallel([
150
+ Animated.timing(fade, {
151
+ toValue: 1,
152
+ duration: Math.round(transitionDuration * 0.55),
153
+ useNativeDriver: true,
154
+ }),
155
+ Animated.timing(zoom, {
156
+ toValue: 1,
157
+ duration: Math.round(transitionDuration * 0.55),
158
+ useNativeDriver: true,
159
+ }),
160
+ ]).start(() => {
161
+ isAnimating.current = false;
162
+ });
163
+ });
164
+ },
165
+ [fade, models.length, transitionDuration, transitionScale, zoom]
166
+ );
167
+
168
+ useEffect(() => {
169
+ if (!autoPlay || models.length <= 1) {
170
+ return;
171
+ }
172
+
173
+ const timeout = setTimeout(() => {
174
+ const nextIndex = (index + 1) % models.length;
175
+ runTransition(nextIndex);
176
+ }, autoPlayInterval);
177
+
178
+ return () => {
179
+ clearTimeout(timeout);
180
+ };
181
+ }, [autoPlay, autoPlayInterval, index, models.length, runTransition]);
182
+
183
+ const next = () => {
184
+ if (models.length === 0) {
185
+ return;
186
+ }
187
+
188
+ const nextIndex = (index + 1) % models.length;
189
+ runTransition(nextIndex);
190
+ };
191
+
192
+ const prev = () => {
193
+ if (models.length === 0) {
194
+ return;
195
+ }
196
+
197
+ const nextIndex = (index - 1 + models.length) % models.length;
198
+ runTransition(nextIndex);
199
+ };
200
+
201
+ const onViewerTouchStart = (event: {
202
+ nativeEvent: { pageX: number; pageY: number };
203
+ }) => {
204
+ if (!enableSwipeNavigation) {
205
+ return;
206
+ }
207
+
208
+ touchStartX.current = event.nativeEvent.pageX;
209
+ touchStartY.current = event.nativeEvent.pageY;
210
+ };
211
+
212
+ const onViewerTouchEnd = (event: {
213
+ nativeEvent: { pageX: number; pageY: number };
214
+ }) => {
215
+ if (
216
+ !enableSwipeNavigation ||
217
+ touchStartX.current === null ||
218
+ touchStartY.current === null
219
+ ) {
220
+ return;
221
+ }
222
+
223
+ const deltaX = event.nativeEvent.pageX - touchStartX.current;
224
+ const deltaY = event.nativeEvent.pageY - touchStartY.current;
225
+
226
+ touchStartX.current = null;
227
+ touchStartY.current = null;
228
+
229
+ if (
230
+ Math.abs(deltaX) < swipeThreshold ||
231
+ Math.abs(deltaX) <= Math.abs(deltaY)
232
+ ) {
233
+ return;
234
+ }
235
+
236
+ if (deltaX < 0) {
237
+ next();
238
+ return;
239
+ }
240
+
241
+ prev();
242
+ };
243
+
244
+ const wrapperStyle = useMemo(
245
+ () => [{ width, height }, containerStyle],
246
+ [containerStyle, height, width]
247
+ );
248
+ const controlsDisabled = models.length <= 1;
249
+ const controlsProps: NavigationButtonRenderProps = {
250
+ onPress: () => undefined,
251
+ disabled: controlsDisabled,
252
+ index,
253
+ total: models.length,
254
+ isAnimating: isAnimating.current,
255
+ };
256
+
257
+ if (models.length === 0) {
258
+ return <View style={wrapperStyle} />;
259
+ }
260
+
261
+ const activeItem = models[index];
262
+ const activeModelPath = getModelPath(activeItem);
263
+ const activeScale = getResolvedValue(
264
+ isConfiguredModel(activeItem) ? activeItem.scale : undefined,
265
+ scale
266
+ );
267
+ const activePosition = getResolvedValue(
268
+ isConfiguredModel(activeItem) ? activeItem.position : undefined,
269
+ position
270
+ );
271
+ const activeCameraPosition = getResolvedValue(
272
+ isConfiguredModel(activeItem) ? activeItem.cameraPosition : undefined,
273
+ cameraPosition
274
+ );
275
+ const shouldRenderPrevButton = !!renderPrevButton || showDefaultButtons;
276
+ const shouldRenderNextButton = !!renderNextButton || showDefaultButtons;
277
+
278
+ return (
279
+ <View style={wrapperStyle}>
280
+ <Animated.View
281
+ onTouchStart={onViewerTouchStart}
282
+ onTouchEnd={onViewerTouchEnd}
283
+ style={[
284
+ styles.viewer,
285
+ {
286
+ opacity: fade,
287
+ transform: [{ scale: zoom }],
288
+ },
289
+ ]}
290
+ >
291
+ <ModelViewer
292
+ modelPath={activeModelPath}
293
+ scale={activeScale}
294
+ position={activePosition}
295
+ cameraPosition={activeCameraPosition}
296
+ fov={fov}
297
+ autoRotate={autoRotate}
298
+ autoRotateSpeed={autoRotateSpeed}
299
+ enablePan={!enableSwipeNavigation}
300
+ />
301
+ </Animated.View>
302
+
303
+ <View style={styles.controls}>
304
+ <View style={styles.buttonsRow}>
305
+ {shouldRenderPrevButton ? (
306
+ renderPrevButton ? (
307
+ renderPrevButton({
308
+ ...controlsProps,
309
+ onPress: prev,
310
+ })
311
+ ) : (
312
+ <Pressable
313
+ onPress={prev}
314
+ disabled={controlsDisabled}
315
+ style={({ pressed }) => [
316
+ styles.navButton,
317
+ pressed && styles.navButtonPressed,
318
+ controlsDisabled && styles.navButtonDisabled,
319
+ ]}
320
+ >
321
+ <Text style={styles.arrow}>{'<'}</Text>
322
+ </Pressable>
323
+ )
324
+ ) : (
325
+ <View />
326
+ )}
327
+
328
+ {shouldRenderNextButton ? (
329
+ renderNextButton ? (
330
+ renderNextButton({
331
+ ...controlsProps,
332
+ onPress: next,
333
+ })
334
+ ) : (
335
+ <Pressable
336
+ onPress={next}
337
+ disabled={controlsDisabled}
338
+ style={({ pressed }) => [
339
+ styles.navButton,
340
+ pressed && styles.navButtonPressed,
341
+ controlsDisabled && styles.navButtonDisabled,
342
+ ]}
343
+ >
344
+ <Text style={styles.arrow}>{'>'}</Text>
345
+ </Pressable>
346
+ )
347
+ ) : (
348
+ <View />
349
+ )}
350
+ </View>
351
+
352
+ {showPaginationDots && models.length > 0 ? (
353
+ <View style={styles.dotsContainer}>
354
+ {models.map((_, modelIndex) => (
355
+ <View
356
+ key={`dot-${modelIndex}`}
357
+ style={[styles.dot, modelIndex === index && styles.dotActive]}
358
+ />
359
+ ))}
360
+ </View>
361
+ ) : null}
362
+ </View>
363
+ </View>
364
+ );
365
+ };
366
+
367
+ const styles = StyleSheet.create({
368
+ viewer: {
369
+ flex: 1,
370
+ },
371
+ controls: {
372
+ position: 'absolute',
373
+ bottom: 24,
374
+ width: '100%',
375
+ gap: 12,
376
+ },
377
+ buttonsRow: {
378
+ flexDirection: 'row',
379
+ justifyContent: 'space-between',
380
+ paddingHorizontal: 20,
381
+ },
382
+ dotsContainer: {
383
+ flexDirection: 'row',
384
+ alignSelf: 'center',
385
+ alignItems: 'center',
386
+ justifyContent: 'center',
387
+ gap: 8,
388
+ },
389
+ dot: {
390
+ width: 8,
391
+ height: 8,
392
+ borderRadius: 4,
393
+ backgroundColor: 'rgba(196, 37, 37, 0.45)',
394
+ },
395
+ dotActive: {
396
+ width: 18,
397
+ borderRadius: 6,
398
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
399
+ },
400
+ arrow: {
401
+ fontSize: 28,
402
+ color: '#fff',
403
+ fontWeight: '700',
404
+ textAlign: 'center',
405
+ lineHeight: 30,
406
+ },
407
+ navButton: {
408
+ width: 52,
409
+ height: 52,
410
+ borderRadius: 26,
411
+ backgroundColor: 'rgba(21, 29, 44, 0.75)',
412
+ alignItems: 'center',
413
+ justifyContent: 'center',
414
+ borderWidth: 1,
415
+ borderColor: 'rgba(255, 255, 255, 0.2)',
416
+ },
417
+ navButtonPressed: {
418
+ opacity: 0.75,
419
+ transform: [{ scale: 0.96 }],
420
+ },
421
+ navButtonDisabled: {
422
+ opacity: 0.5,
423
+ },
424
+ });
425
+
426
+ export default ModelCarousel;
@@ -0,0 +1,60 @@
1
+ import { createElement, Suspense } from 'react';
2
+ import { Canvas } from '@react-three/fiber/native';
3
+ import { OrbitControls } from '@react-three/drei/native';
4
+ import { useModelLoader } from '../hooks/useModelLoader';
5
+
6
+ type Props = {
7
+ modelPath: unknown;
8
+ scale?: number;
9
+ position?: number[];
10
+
11
+ cameraPosition?: number[];
12
+ fov?: number;
13
+
14
+ autoRotate?: boolean;
15
+ autoRotateSpeed?: number;
16
+
17
+ enableZoom?: boolean;
18
+ enablePan?: boolean;
19
+ enableRotate?: boolean;
20
+ };
21
+
22
+ const ModelViewer = ({
23
+ modelPath,
24
+ scale = 0.8,
25
+ position = [0, -2.7, 0],
26
+
27
+ cameraPosition = [0, 9, 5],
28
+ fov = 35,
29
+
30
+ autoRotate = true,
31
+ autoRotateSpeed = 10,
32
+
33
+ enableZoom = true,
34
+ enablePan = true,
35
+ enableRotate = true,
36
+ }: Props) => {
37
+ const scene = useModelLoader(modelPath);
38
+
39
+ return (
40
+ <Canvas camera={{ position: cameraPosition, fov }}>
41
+ {createElement('ambientLight' as any, { intensity: 2.5 })}
42
+
43
+ <OrbitControls
44
+ enableRotate={enableRotate}
45
+ enableZoom={enableZoom}
46
+ enablePan={enablePan}
47
+ maxPolarAngle={Math.PI / 2}
48
+ minPolarAngle={Math.PI / 2}
49
+ autoRotate={autoRotate}
50
+ autoRotateSpeed={autoRotateSpeed}
51
+ />
52
+
53
+ <Suspense fallback={null}>
54
+ {createElement('primitive' as any, { object: scene, scale, position })}
55
+ </Suspense>
56
+ </Canvas>
57
+ );
58
+ };
59
+
60
+ export default ModelViewer;
@@ -0,0 +1,7 @@
1
+ import { useGLTF } from '@react-three/drei/native';
2
+
3
+ export const useModelLoader = (modelPath: unknown) => {
4
+ const { scene } = useGLTF(modelPath as string);
5
+
6
+ return scene;
7
+ };
package/src/index.tsx ADDED
@@ -0,0 +1 @@
1
+ export { default as ModelCarousel } from './components/ModelCarousel';
@@ -0,0 +1,3 @@
1
+ export function multiply(a: number, b: number): number {
2
+ return a * b;
3
+ }
@@ -0,0 +1,9 @@
1
+ declare module '*.glb' {
2
+ const content: any;
3
+ export default content;
4
+ }
5
+
6
+ declare module '*.gltf' {
7
+ const content: any;
8
+ export default content;
9
+ }