react-native-morph-card 0.1.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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/android/build.gradle +59 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/java/com/melivalesca/morphcard/MorphCardModule.kt +120 -0
  6. package/android/src/main/java/com/melivalesca/morphcard/MorphCardPackage.kt +42 -0
  7. package/android/src/main/java/com/melivalesca/morphcard/MorphCardSourceManager.kt +40 -0
  8. package/android/src/main/java/com/melivalesca/morphcard/MorphCardSourceView.kt +755 -0
  9. package/android/src/main/java/com/melivalesca/morphcard/MorphCardTargetManager.kt +48 -0
  10. package/android/src/main/java/com/melivalesca/morphcard/MorphCardTargetView.kt +159 -0
  11. package/android/src/main/java/com/melivalesca/morphcard/MorphCardViewRegistry.kt +24 -0
  12. package/android/src/main/jni/CMakeLists.txt +62 -0
  13. package/common/cpp/react/renderer/components/morphcard/RNCMorphCardState.h +30 -0
  14. package/ios/Fabric/RNCMorphCardSourceComponentView.h +25 -0
  15. package/ios/Fabric/RNCMorphCardSourceComponentView.mm +582 -0
  16. package/ios/Fabric/RNCMorphCardTargetComponentView.h +20 -0
  17. package/ios/Fabric/RNCMorphCardTargetComponentView.mm +99 -0
  18. package/ios/RNCMorphCardModule.h +14 -0
  19. package/ios/RNCMorphCardModule.mm +126 -0
  20. package/ios/RNCMorphCardSource.h +23 -0
  21. package/ios/RNCMorphCardSource.m +144 -0
  22. package/ios/RNCMorphCardSourceManager.h +5 -0
  23. package/ios/RNCMorphCardSourceManager.m +17 -0
  24. package/ios/RNCMorphCardTarget.h +19 -0
  25. package/ios/RNCMorphCardTarget.m +27 -0
  26. package/ios/RNCMorphCardTargetManager.h +5 -0
  27. package/ios/RNCMorphCardTargetManager.m +16 -0
  28. package/ios/RNCMorphCardViewRegistry.h +35 -0
  29. package/ios/RNCMorphCardViewRegistry.m +40 -0
  30. package/lib/commonjs/MorphCard.types.js +6 -0
  31. package/lib/commonjs/MorphCard.types.js.map +1 -0
  32. package/lib/commonjs/MorphCardSource.js +95 -0
  33. package/lib/commonjs/MorphCardSource.js.map +1 -0
  34. package/lib/commonjs/MorphCardTarget.js +83 -0
  35. package/lib/commonjs/MorphCardTarget.js.map +1 -0
  36. package/lib/commonjs/index.js +45 -0
  37. package/lib/commonjs/index.js.map +1 -0
  38. package/lib/commonjs/package.json +1 -0
  39. package/lib/commonjs/specs/NativeMorphCardModule.js +9 -0
  40. package/lib/commonjs/specs/NativeMorphCardModule.js.map +1 -0
  41. package/lib/commonjs/specs/NativeMorphCardSource.js +10 -0
  42. package/lib/commonjs/specs/NativeMorphCardSource.js.map +1 -0
  43. package/lib/commonjs/specs/NativeMorphCardTarget.js +10 -0
  44. package/lib/commonjs/specs/NativeMorphCardTarget.js.map +1 -0
  45. package/lib/commonjs/useMorphTarget.js +28 -0
  46. package/lib/commonjs/useMorphTarget.js.map +1 -0
  47. package/lib/module/MorphCard.types.js +4 -0
  48. package/lib/module/MorphCard.types.js.map +1 -0
  49. package/lib/module/MorphCardSource.js +85 -0
  50. package/lib/module/MorphCardSource.js.map +1 -0
  51. package/lib/module/MorphCardTarget.js +76 -0
  52. package/lib/module/MorphCardTarget.js.map +1 -0
  53. package/lib/module/index.js +6 -0
  54. package/lib/module/index.js.map +1 -0
  55. package/lib/module/package.json +1 -0
  56. package/lib/module/specs/NativeMorphCardModule.js +5 -0
  57. package/lib/module/specs/NativeMorphCardModule.js.map +1 -0
  58. package/lib/module/specs/NativeMorphCardSource.js +5 -0
  59. package/lib/module/specs/NativeMorphCardSource.js.map +1 -0
  60. package/lib/module/specs/NativeMorphCardTarget.js +5 -0
  61. package/lib/module/specs/NativeMorphCardTarget.js.map +1 -0
  62. package/lib/module/useMorphTarget.js +22 -0
  63. package/lib/module/useMorphTarget.js.map +1 -0
  64. package/lib/typescript/src/MorphCard.types.d.ts +29 -0
  65. package/lib/typescript/src/MorphCard.types.d.ts.map +1 -0
  66. package/lib/typescript/src/MorphCardSource.d.ts +35 -0
  67. package/lib/typescript/src/MorphCardSource.d.ts.map +1 -0
  68. package/lib/typescript/src/MorphCardTarget.d.ts +20 -0
  69. package/lib/typescript/src/MorphCardTarget.d.ts.map +1 -0
  70. package/lib/typescript/src/index.d.ts +6 -0
  71. package/lib/typescript/src/index.d.ts.map +1 -0
  72. package/lib/typescript/src/specs/NativeMorphCardModule.d.ts +14 -0
  73. package/lib/typescript/src/specs/NativeMorphCardModule.d.ts.map +1 -0
  74. package/lib/typescript/src/specs/NativeMorphCardSource.d.ts +13 -0
  75. package/lib/typescript/src/specs/NativeMorphCardSource.d.ts.map +1 -0
  76. package/lib/typescript/src/specs/NativeMorphCardTarget.d.ts +25 -0
  77. package/lib/typescript/src/specs/NativeMorphCardTarget.d.ts.map +1 -0
  78. package/lib/typescript/src/useMorphTarget.d.ts +16 -0
  79. package/lib/typescript/src/useMorphTarget.d.ts.map +1 -0
  80. package/package.json +101 -0
  81. package/react-native-morph-card.podspec +41 -0
  82. package/react-native.config.js +13 -0
  83. package/src/MorphCard.types.ts +29 -0
  84. package/src/MorphCardSource.tsx +105 -0
  85. package/src/MorphCardTarget.tsx +127 -0
  86. package/src/index.tsx +10 -0
  87. package/src/specs/NativeMorphCardModule.ts +21 -0
  88. package/src/specs/NativeMorphCardSource.ts +20 -0
  89. package/src/specs/NativeMorphCardTarget.ts +38 -0
  90. package/src/useMorphTarget.ts +21 -0
@@ -0,0 +1,13 @@
1
+ import type { ViewProps } from 'react-native';
2
+ import type { DirectEventHandler, Double, WithDefault } from 'react-native/Libraries/Types/CodegenTypes';
3
+ export interface NativeMorphCardSourceProps extends ViewProps {
4
+ onMorphStart?: DirectEventHandler<{}>;
5
+ onMorphComplete?: DirectEventHandler<{}>;
6
+ onDismissComplete?: DirectEventHandler<{}>;
7
+ duration?: Double;
8
+ scaleMode?: WithDefault<'aspectFill' | 'aspectFit' | 'stretch', 'aspectFill'>;
9
+ cardBorderRadius?: Double;
10
+ }
11
+ declare const _default: import("react-native/Libraries/Utilities/codegenNativeComponent").NativeComponentType<NativeMorphCardSourceProps>;
12
+ export default _default;
13
+ //# sourceMappingURL=NativeMorphCardSource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativeMorphCardSource.d.ts","sourceRoot":"","sources":["../../../../src/specs/NativeMorphCardSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EACV,kBAAkB,EAClB,MAAM,EACN,WAAW,EACZ,MAAM,2CAA2C,CAAC;AAGnD,MAAM,WAAW,0BAA2B,SAAQ,SAAS;IAC3D,YAAY,CAAC,EAAE,kBAAkB,CAAC,EAAE,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,kBAAkB,CAAC,EAAE,CAAC,CAAC;IACzC,iBAAiB,CAAC,EAAE,kBAAkB,CAAC,EAAE,CAAC,CAAC;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,WAAW,CAAC,YAAY,GAAG,WAAW,GAAG,SAAS,EAAE,YAAY,CAAC,CAAC;IAC9E,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;;AAED,wBAEE"}
@@ -0,0 +1,25 @@
1
+ import type { ViewProps } from 'react-native';
2
+ import type { DirectEventHandler, Double, Int32 } from 'react-native/Libraries/Types/CodegenTypes';
3
+ interface MorphCompleteEvent {
4
+ finished: boolean;
5
+ }
6
+ export interface NativeMorphCardTargetProps extends ViewProps {
7
+ /**
8
+ * Duration of the morph animation in milliseconds.
9
+ */
10
+ duration?: Double;
11
+ /**
12
+ * The react tag of the source card to morph from.
13
+ */
14
+ sourceTag?: Int32;
15
+ onMorphComplete?: DirectEventHandler<MorphCompleteEvent>;
16
+ /** Explicit target width. 0 = use source width. */
17
+ targetWidth?: Double;
18
+ /** Explicit target height. 0 = use source height. */
19
+ targetHeight?: Double;
20
+ /** Target border radius. -1 = use source border radius. */
21
+ targetBorderRadius?: Double;
22
+ }
23
+ declare const _default: import("react-native/Libraries/Utilities/codegenNativeComponent").NativeComponentType<NativeMorphCardTargetProps>;
24
+ export default _default;
25
+ //# sourceMappingURL=NativeMorphCardTarget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativeMorphCardTarget.d.ts","sourceRoot":"","sources":["../../../../src/specs/NativeMorphCardTarget.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EACV,kBAAkB,EAClB,MAAM,EACN,KAAK,EACN,MAAM,2CAA2C,CAAC;AAGnD,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,0BAA2B,SAAQ,SAAS;IAC3D;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC;IAElB,eAAe,CAAC,EAAE,kBAAkB,CAAC,kBAAkB,CAAC,CAAC;IAEzD,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,2DAA2D;IAC3D,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;;AAED,wBAEE"}
@@ -0,0 +1,16 @@
1
+ interface UseMorphTargetOptions {
2
+ sourceTag: number;
3
+ navigation: {
4
+ goBack: () => void;
5
+ };
6
+ }
7
+ /**
8
+ * Hook for detail screens using MorphCardTarget.
9
+ *
10
+ * Returns a `dismiss()` that collapses the morph and navigates back.
11
+ */
12
+ export declare function useMorphTarget({ sourceTag, navigation }: UseMorphTargetOptions): {
13
+ dismiss: () => Promise<void>;
14
+ };
15
+ export {};
16
+ //# sourceMappingURL=useMorphTarget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMorphTarget.d.ts","sourceRoot":"","sources":["../../../src/useMorphTarget.ts"],"names":[],"mappings":"AAGA,UAAU,qBAAqB;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE;QAAE,MAAM,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;CACpC;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,qBAAqB;;EAO9E"}
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "react-native-morph-card",
3
+ "version": "0.1.0",
4
+ "description": "Native card-to-modal morph transition for React Native. iOS App Store-style expand animation.",
5
+ "main": "lib/commonjs/index.js",
6
+ "module": "lib/module/index.js",
7
+ "react-native": "src/index.tsx",
8
+ "types": "lib/typescript/src/index.d.ts",
9
+ "source": "src/index.tsx",
10
+ "files": [
11
+ "src",
12
+ "lib",
13
+ "!**/__tests__",
14
+ "ios",
15
+ "android",
16
+ "common",
17
+ "react-native-morph-card.podspec",
18
+ "react-native.config.js",
19
+ "!ios/build",
20
+ "!android/build",
21
+ "!android/.gradle",
22
+ "!**/*.test.*"
23
+ ],
24
+ "scripts": {
25
+ "prepare": "bob build",
26
+ "test": "yarn validate:eslint && yarn validate:typescript && yarn validate:jest",
27
+ "validate:eslint": "eslint \"src/**/*.{js,ts,tsx}\"",
28
+ "validate:typescript": "tsc --noEmit",
29
+ "validate:jest": "jest",
30
+ "format:prettier:write": "prettier --write \"src/**/*.{js,ts,tsx}\"",
31
+ "format:prettier:check": "prettier --check \"src/**/*.{js,ts,tsx}\"",
32
+ "format:clang:write": "find ios common -iname '*.h' -o -iname '*.m' -o -iname '*.mm' -o -iname '*.cpp' | xargs clang-format -i",
33
+ "format:clang:check": "find ios common -iname '*.h' -o -iname '*.m' -o -iname '*.mm' -o -iname '*.cpp' | xargs clang-format --dry-run --Werror",
34
+ "format:spotless:write": "cd android && ./gradlew spotlessApply",
35
+ "format:spotless:check": "cd android && ./gradlew spotlessCheck",
36
+ "format:write": "yarn format:prettier:write && yarn format:clang:write && yarn format:spotless:write",
37
+ "format:check": "yarn format:prettier:check && yarn format:clang:check && yarn format:spotless:check"
38
+ },
39
+ "keywords": [
40
+ "react-native",
41
+ "morph",
42
+ "card",
43
+ "modal",
44
+ "transition",
45
+ "animation",
46
+ "shared-element",
47
+ "hero",
48
+ "ios",
49
+ "android"
50
+ ],
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/MeliValesca/react-native-morph-card.git"
54
+ },
55
+ "author": "MeliValesca (https://github.com/MeliValesca)",
56
+ "license": "MIT",
57
+ "bugs": {
58
+ "url": "https://github.com/MeliValesca/react-native-morph-card/issues"
59
+ },
60
+ "homepage": "https://github.com/MeliValesca/react-native-morph-card#readme",
61
+ "peerDependencies": {
62
+ "react": "*",
63
+ "react-native": "*"
64
+ },
65
+ "devDependencies": {
66
+ "@react-native/babel-preset": "^0.80.0",
67
+ "@react-native/eslint-config": "^0.80.0",
68
+ "@react-native/eslint-plugin-specs": "^0.80.0",
69
+ "@types/react": "^19.0.0",
70
+ "eslint": "^8.57.0",
71
+ "jest": "^29.7.0",
72
+ "prettier": "^3.3.0",
73
+ "react": "^19.1.0",
74
+ "react-native": "^0.80.0",
75
+ "react-native-builder-bob": "^0.35.0",
76
+ "typescript": "^5.5.0"
77
+ },
78
+ "codegenConfig": {
79
+ "android": {
80
+ "javaPackageName": "com.melivalesca.morphcard"
81
+ },
82
+ "ios": {
83
+ "componentProvider": {
84
+ "RNCMorphCardSource": "RNCMorphCardSourceComponentView",
85
+ "RNCMorphCardTarget": "RNCMorphCardTargetComponentView"
86
+ }
87
+ },
88
+ "name": "morphcard",
89
+ "type": "all",
90
+ "jsSrcsDir": "./src/specs"
91
+ },
92
+ "react-native-builder-bob": {
93
+ "source": "src",
94
+ "output": "lib",
95
+ "targets": [
96
+ "commonjs",
97
+ "module",
98
+ "typescript"
99
+ ]
100
+ }
101
+ }
@@ -0,0 +1,41 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
+
5
+ fabric_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
6
+
7
+ Pod::Spec.new do |s|
8
+ s.name = 'react-native-morph-card'
9
+ s.version = package['version']
10
+ s.summary = package['description']
11
+ s.homepage = package['homepage']
12
+ s.license = package['license']
13
+ s.authors = package['author']
14
+ s.platforms = { :ios => '15.1', :tvos => '15.1', :visionos => '1.0' }
15
+ s.source = { :git => package['repository']['url'], :tag => "v#{s.version}" }
16
+
17
+ s.source_files = 'ios/*.{h,m,mm}'
18
+
19
+ if fabric_enabled
20
+ install_modules_dependencies(s)
21
+
22
+ s.subspec 'common' do |ss|
23
+ ss.source_files = 'common/cpp/**/*.{cpp,h}'
24
+ ss.header_dir = 'react/renderer/components/morphcard'
25
+ ss.pod_target_xcconfig = {
26
+ 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/common/cpp"',
27
+ }
28
+ end
29
+
30
+ s.subspec 'fabric' do |ss|
31
+ ss.dependency "#{s.name}/common"
32
+ ss.source_files = 'ios/Fabric/**/*.{h,m,mm}'
33
+ ss.pod_target_xcconfig = {
34
+ 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/common/cpp"',
35
+ }
36
+ end
37
+ else
38
+ s.exclude_files = 'ios/Fabric/**'
39
+ s.dependency 'React-Core'
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ android: {
5
+ libraryName: 'morphcard',
6
+ componentDescriptors: [
7
+ 'RNCMorphCardSourceComponentDescriptor',
8
+ 'RNCMorphCardTargetComponentDescriptor',
9
+ ],
10
+ },
11
+ },
12
+ },
13
+ };
@@ -0,0 +1,29 @@
1
+ import type { ViewProps } from 'react-native';
2
+
3
+ export interface MorphCardProps extends ViewProps {
4
+ /**
5
+ * Duration of the morph animation in milliseconds. Defaults to 500.
6
+ */
7
+ duration?: number;
8
+ /**
9
+ * Called when the expand animation begins.
10
+ */
11
+ onMorphStart?: () => void;
12
+ /**
13
+ * Called when the expand animation completes.
14
+ */
15
+ onMorphComplete?: () => void;
16
+ /**
17
+ * Called when the collapse animation completes.
18
+ */
19
+ onDismissComplete?: () => void;
20
+ /**
21
+ * The collapsed card content.
22
+ */
23
+ renderCollapsed: () => React.ReactNode;
24
+ /**
25
+ * The expanded fullscreen content.
26
+ */
27
+ renderExpanded: (collapse: () => void) => React.ReactNode;
28
+ ref?: React.Ref<any>;
29
+ }
@@ -0,0 +1,105 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Pressable,
4
+ type ViewStyle,
5
+ type DimensionValue,
6
+ View,
7
+ findNodeHandle,
8
+ } from 'react-native';
9
+ import NativeMorphCardModule from './specs/NativeMorphCardModule';
10
+ import NativeSourceViewSpec from './specs/NativeMorphCardSource';
11
+
12
+ const NativeSourceView = NativeSourceViewSpec ?? View;
13
+
14
+ export type ScaleMode = 'aspectFill' | 'aspectFit' | 'stretch';
15
+
16
+ export interface MorphCardSourceProps {
17
+ ref?: React.Ref<any>;
18
+ duration?: number;
19
+ width?: DimensionValue;
20
+ height?: DimensionValue;
21
+ borderRadius?: number;
22
+ backgroundColor?: string;
23
+ /** How the snapshot scales in no-wrapper mode (no backgroundColor). Default: 'aspectFill' */
24
+ scaleMode?: ScaleMode;
25
+ onPress?: (sourceTag: number) => void;
26
+ children: React.ReactNode;
27
+ }
28
+
29
+ export const MorphCardSource = ({
30
+ children,
31
+ duration = 300,
32
+ width,
33
+ height,
34
+ borderRadius,
35
+ backgroundColor,
36
+ scaleMode,
37
+ onPress,
38
+ ref,
39
+ }: MorphCardSourceProps) => {
40
+ const nativeRef = React.useRef<any>(null);
41
+ React.useImperativeHandle(ref, () => nativeRef.current);
42
+
43
+ const style: ViewStyle = {};
44
+ if (width != null) style.width = width as ViewStyle['width'];
45
+ if (height != null) style.height = height as ViewStyle['height'];
46
+ if (borderRadius != null) {
47
+ style.borderRadius = borderRadius;
48
+ style.overflow = 'hidden';
49
+ }
50
+ if (backgroundColor != null) style.backgroundColor = backgroundColor;
51
+ const handlePress = React.useCallback(() => {
52
+ if (!onPress) return;
53
+ const tag = findNodeHandle(nativeRef.current);
54
+ if (tag != null) {
55
+ // Create overlay immediately BEFORE navigation to prevent target screen flash
56
+ NativeMorphCardModule.prepareExpand(tag);
57
+ onPress(tag);
58
+ }
59
+ }, [onPress]);
60
+
61
+ const content = (
62
+ <NativeSourceView ref={nativeRef} duration={duration} scaleMode={scaleMode} cardBorderRadius={borderRadius} style={style}>
63
+ {children}
64
+ </NativeSourceView>
65
+ );
66
+
67
+ if (onPress) {
68
+ return <Pressable onPress={handlePress}>{content}</Pressable>;
69
+ }
70
+
71
+ return content;
72
+ };
73
+
74
+ /**
75
+ * Get the native view tag from a ref. Useful for passing sourceTag
76
+ * to the detail screen via navigation params.
77
+ */
78
+ export function getViewTag(viewRef: React.RefObject<any>): number | null {
79
+ return findNodeHandle(viewRef.current);
80
+ }
81
+
82
+ /**
83
+ * Expand: background grows from card bounds to fullscreen while
84
+ * card snapshot moves to targetRef's position. Content fades in at the end.
85
+ *
86
+ * Call this AFTER navigating to the detail screen (so the target is mounted).
87
+ */
88
+ export async function morphExpand(
89
+ sourceRef: React.RefObject<any>,
90
+ targetRef: React.RefObject<any>,
91
+ ): Promise<boolean> {
92
+ const sourceTag = findNodeHandle(sourceRef.current);
93
+ const targetTag = findNodeHandle(targetRef.current);
94
+ if (!sourceTag || !targetTag) return false;
95
+ return NativeMorphCardModule.expand(sourceTag, targetTag);
96
+ }
97
+
98
+ /**
99
+ * Collapse: content fades out, background shrinks from fullscreen back
100
+ * to card bounds while card snapshot moves from target back to card position.
101
+ * Uses the target stored from the last expand call.
102
+ */
103
+ export async function morphCollapse(sourceTag: number): Promise<boolean> {
104
+ return NativeMorphCardModule.collapse(sourceTag);
105
+ }
@@ -0,0 +1,127 @@
1
+ import * as React from 'react';
2
+ import {
3
+ requireNativeComponent,
4
+ type ViewProps,
5
+ type StyleProp,
6
+ type ViewStyle,
7
+ type DimensionValue,
8
+ View,
9
+ findNodeHandle,
10
+ type LayoutChangeEvent,
11
+ } from 'react-native';
12
+ import NativeMorphCardModule from './specs/NativeMorphCardModule';
13
+
14
+ let NativeTargetView: React.ComponentType<
15
+ ViewProps & {
16
+ sourceTag?: number;
17
+ targetWidth?: number;
18
+ targetHeight?: number;
19
+ targetBorderRadius?: number;
20
+ }
21
+ >;
22
+
23
+ try {
24
+ NativeTargetView = requireNativeComponent('RNCMorphCardTarget');
25
+ } catch {
26
+ NativeTargetView = View;
27
+ }
28
+
29
+ export interface MorphCardTargetProps {
30
+ /** The sourceTag from route params — triggers expand on mount. */
31
+ sourceTag: number;
32
+ /** Optional width override (number or '100%'). If omitted, source width is used. */
33
+ width?: DimensionValue;
34
+ /** Optional height override (number or '100%'). If omitted, source height is used. */
35
+ height?: DimensionValue;
36
+ /** Optional border radius override. If omitted, source border radius is used. Set to 0 for no rounding. */
37
+ borderRadius?: number;
38
+ /** Vertical offset for the content snapshot inside the expanded wrapper (wrapper mode only). */
39
+ contentOffsetY?: number;
40
+ /** Center the content snapshot horizontally inside the expanded wrapper (wrapper mode only). */
41
+ contentCentered?: boolean;
42
+ /** Optional style for positioning (margin, position, etc). */
43
+ style?: StyleProp<ViewStyle>;
44
+ }
45
+
46
+ export const MorphCardTarget = ({
47
+ sourceTag,
48
+ width,
49
+ height,
50
+ borderRadius,
51
+ contentOffsetY,
52
+ contentCentered,
53
+ style,
54
+ ...rest
55
+ }: MorphCardTargetProps) => {
56
+ const nativeRef = React.useRef<any>(null);
57
+ const expandedRef = React.useRef(false);
58
+ const [sourceSize, setSourceSize] = React.useState<{
59
+ width: number;
60
+ height: number;
61
+ } | null>(null);
62
+
63
+ // Fetch source size for auto-sizing when width/height not provided
64
+ React.useEffect(() => {
65
+ let cancelled = false;
66
+ if (sourceTag && (width == null || height == null)) {
67
+ NativeMorphCardModule.getSourceSize(sourceTag)
68
+ .then((size: { width: number; height: number }) => {
69
+ if (!cancelled) setSourceSize(size);
70
+ })
71
+ .catch(() => {});
72
+ }
73
+ return () => {
74
+ cancelled = true;
75
+ };
76
+ }, [sourceTag, width, height]);
77
+
78
+ // Use onLayout to get resolved pixel dimensions, then trigger expand
79
+ const handleLayout = React.useCallback(
80
+ (e: LayoutChangeEvent) => {
81
+ if (expandedRef.current) return;
82
+ if (!sourceTag) return;
83
+
84
+ const { width: lw, height: lh } = e.nativeEvent.layout;
85
+ const targetTag = findNodeHandle(nativeRef.current);
86
+ if (!targetTag) return;
87
+
88
+ expandedRef.current = true;
89
+
90
+ NativeMorphCardModule.setTargetConfig(
91
+ sourceTag,
92
+ lw,
93
+ lh,
94
+ borderRadius != null ? borderRadius : -1,
95
+ contentOffsetY ?? 0,
96
+ contentCentered ?? false
97
+ );
98
+ NativeMorphCardModule.expand(sourceTag, targetTag);
99
+ },
100
+ [sourceTag, borderRadius, contentOffsetY, contentCentered]
101
+ );
102
+
103
+ const sizeStyle: ViewStyle = {};
104
+ if (width != null) {
105
+ sizeStyle.width = width;
106
+ } else if (sourceSize) {
107
+ sizeStyle.width = sourceSize.width;
108
+ }
109
+ if (height != null) {
110
+ sizeStyle.height = height;
111
+ } else if (sourceSize) {
112
+ sizeStyle.height = sourceSize.height;
113
+ }
114
+
115
+ return (
116
+ <NativeTargetView
117
+ ref={nativeRef}
118
+ sourceTag={sourceTag}
119
+ targetWidth={0}
120
+ targetHeight={0}
121
+ targetBorderRadius={borderRadius != null ? borderRadius : -1}
122
+ style={[style, sizeStyle]}
123
+ onLayout={handleLayout}
124
+ {...rest}
125
+ />
126
+ );
127
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,10 @@
1
+ export {
2
+ MorphCardSource,
3
+ morphExpand,
4
+ morphCollapse,
5
+ getViewTag,
6
+ } from './MorphCardSource';
7
+ export type { MorphCardSourceProps, ScaleMode } from './MorphCardSource';
8
+ export { MorphCardTarget } from './MorphCardTarget';
9
+ export type { MorphCardTargetProps } from './MorphCardTarget';
10
+ export { useMorphTarget } from './useMorphTarget';
@@ -0,0 +1,21 @@
1
+ import type { TurboModule } from 'react-native';
2
+ import { TurboModuleRegistry } from 'react-native';
3
+
4
+ export interface Spec extends TurboModule {
5
+ prepareExpand(sourceTag: number): void;
6
+ expand(sourceTag: number, targetTag: number): Promise<boolean>;
7
+ setTargetConfig(
8
+ sourceTag: number,
9
+ targetWidth: number,
10
+ targetHeight: number,
11
+ targetBorderRadius: number,
12
+ contentOffsetY: number,
13
+ contentCentered: boolean
14
+ ): void;
15
+ collapse(sourceTag: number): Promise<boolean>;
16
+ getSourceSize(
17
+ sourceTag: number
18
+ ): Promise<{ width: number; height: number }>;
19
+ }
20
+
21
+ export default TurboModuleRegistry.getEnforcing<Spec>('RNCMorphCardModule');
@@ -0,0 +1,20 @@
1
+ import type { ViewProps } from 'react-native';
2
+ import type {
3
+ DirectEventHandler,
4
+ Double,
5
+ WithDefault,
6
+ } from 'react-native/Libraries/Types/CodegenTypes';
7
+ import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
8
+
9
+ export interface NativeMorphCardSourceProps extends ViewProps {
10
+ onMorphStart?: DirectEventHandler<{}>;
11
+ onMorphComplete?: DirectEventHandler<{}>;
12
+ onDismissComplete?: DirectEventHandler<{}>;
13
+ duration?: Double;
14
+ scaleMode?: WithDefault<'aspectFill' | 'aspectFit' | 'stretch', 'aspectFill'>;
15
+ cardBorderRadius?: Double;
16
+ }
17
+
18
+ export default codegenNativeComponent<NativeMorphCardSourceProps>(
19
+ 'RNCMorphCardSource',
20
+ );
@@ -0,0 +1,38 @@
1
+ import type { ViewProps } from 'react-native';
2
+ import type {
3
+ DirectEventHandler,
4
+ Double,
5
+ Int32,
6
+ } from 'react-native/Libraries/Types/CodegenTypes';
7
+ import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
8
+
9
+ interface MorphCompleteEvent {
10
+ finished: boolean;
11
+ }
12
+
13
+ export interface NativeMorphCardTargetProps extends ViewProps {
14
+ /**
15
+ * Duration of the morph animation in milliseconds.
16
+ */
17
+ duration?: Double;
18
+
19
+ /**
20
+ * The react tag of the source card to morph from.
21
+ */
22
+ sourceTag?: Int32;
23
+
24
+ onMorphComplete?: DirectEventHandler<MorphCompleteEvent>;
25
+
26
+ /** Explicit target width. 0 = use source width. */
27
+ targetWidth?: Double;
28
+
29
+ /** Explicit target height. 0 = use source height. */
30
+ targetHeight?: Double;
31
+
32
+ /** Target border radius. -1 = use source border radius. */
33
+ targetBorderRadius?: Double;
34
+ }
35
+
36
+ export default codegenNativeComponent<NativeMorphCardTargetProps>(
37
+ 'RNCMorphCardTarget',
38
+ );
@@ -0,0 +1,21 @@
1
+ import * as React from 'react';
2
+ import NativeMorphCardModule from './specs/NativeMorphCardModule';
3
+
4
+ interface UseMorphTargetOptions {
5
+ sourceTag: number;
6
+ navigation: { goBack: () => void };
7
+ }
8
+
9
+ /**
10
+ * Hook for detail screens using MorphCardTarget.
11
+ *
12
+ * Returns a `dismiss()` that collapses the morph and navigates back.
13
+ */
14
+ export function useMorphTarget({ sourceTag, navigation }: UseMorphTargetOptions) {
15
+ const dismiss = React.useCallback(async () => {
16
+ await NativeMorphCardModule.collapse(sourceTag);
17
+ navigation.goBack();
18
+ }, [sourceTag, navigation]);
19
+
20
+ return { dismiss };
21
+ }