11import {
22 useCallback ,
3+ useEffect ,
34 useRef ,
45 type CSSProperties ,
56 type PointerEvent ,
@@ -24,6 +25,21 @@ type ProfileCardProps = {
2425 onContactClick ?: ( ) => void ;
2526} ;
2627
28+ type DeviceOrientationPermissionEvent = DeviceOrientationEventConstructor & {
29+ requestPermission ?: ( ) => Promise < PermissionState > ;
30+ } ;
31+
32+ const clamp = ( value : number , min : number , max : number ) =>
33+ Math . min ( Math . max ( value , min ) , max ) ;
34+
35+ const resetTilt = ( node : HTMLElement ) => {
36+ node . style . setProperty ( "--profile-x" , "50%" ) ;
37+ node . style . setProperty ( "--profile-y" , "50%" ) ;
38+ node . style . setProperty ( "--profile-rotate-x" , "0deg" ) ;
39+ node . style . setProperty ( "--profile-rotate-y" , "0deg" ) ;
40+ node . style . setProperty ( "--profile-glow-opacity" , "0" ) ;
41+ } ;
42+
2743const ProfileCard = ( {
2844 avatarUrl,
2945 iconUrl,
@@ -43,6 +59,79 @@ const ProfileCard = ({
4359 onContactClick,
4460} : ProfileCardProps ) => {
4561 const ref = useRef < HTMLDivElement > ( null ) ;
62+ const orientationEnabledRef = useRef ( false ) ;
63+ const orientationPermissionRequestedRef = useRef ( false ) ;
64+
65+ useEffect ( ( ) => {
66+ if ( ! enableTilt ) return ;
67+
68+ const reduceMotionQuery = window . matchMedia ( "(prefers-reduced-motion: reduce)" ) ;
69+ const mobileQuery = window . matchMedia (
70+ "(hover: none) and (pointer: coarse) and (max-width: 768px)" ,
71+ ) ;
72+ if ( reduceMotionQuery . matches || ! mobileQuery . matches ) return ;
73+
74+ const handleOrientation = ( event : DeviceOrientationEvent ) => {
75+ const node = ref . current ;
76+ if ( ! node || event . beta === null || event . gamma === null ) return ;
77+
78+ const beta = clamp ( event . beta , - 35 , 35 ) ;
79+ const gamma = clamp ( event . gamma , - 25 , 25 ) ;
80+ const percentX = clamp ( 50 + gamma * 1.6 , 18 , 82 ) ;
81+ const percentY = clamp ( 50 + beta * 1.15 , 18 , 82 ) ;
82+ const rotateX = ( beta / 35 ) * - 6 ;
83+ const rotateY = ( gamma / 25 ) * 6 ;
84+
85+ node . style . setProperty ( "--profile-x" , `${ percentX } %` ) ;
86+ node . style . setProperty ( "--profile-y" , `${ percentY } %` ) ;
87+ node . style . setProperty ( "--profile-rotate-x" , `${ rotateX } deg` ) ;
88+ node . style . setProperty ( "--profile-rotate-y" , `${ rotateY } deg` ) ;
89+ node . style . setProperty ( "--profile-glow-opacity" , "1" ) ;
90+ } ;
91+
92+ const startOrientation = ( ) => {
93+ if ( orientationEnabledRef . current ) return ;
94+ orientationEnabledRef . current = true ;
95+ window . addEventListener ( "deviceorientation" , handleOrientation , true ) ;
96+ } ;
97+
98+ const requestOrientationPermission = ( ) => {
99+ if ( orientationPermissionRequestedRef . current ) return ;
100+ orientationPermissionRequestedRef . current = true ;
101+
102+ const orientationEvent = window . DeviceOrientationEvent as
103+ | DeviceOrientationPermissionEvent
104+ | undefined ;
105+ if ( ! orientationEvent ?. requestPermission ) {
106+ startOrientation ( ) ;
107+ return ;
108+ }
109+
110+ void orientationEvent
111+ . requestPermission ( )
112+ . then ( ( state ) => {
113+ if ( state === "granted" ) startOrientation ( ) ;
114+ } )
115+ . catch ( ( ) => {
116+ orientationPermissionRequestedRef . current = false ;
117+ } ) ;
118+ } ;
119+
120+ requestOrientationPermission ( ) ;
121+ window . addEventListener ( "pointerdown" , requestOrientationPermission , {
122+ once : true ,
123+ passive : true ,
124+ } ) ;
125+
126+ return ( ) => {
127+ orientationEnabledRef . current = false ;
128+ window . removeEventListener ( "deviceorientation" , handleOrientation , true ) ;
129+ window . removeEventListener ( "pointerdown" , requestOrientationPermission ) ;
130+
131+ const node = ref . current ;
132+ if ( node ) resetTilt ( node ) ;
133+ } ;
134+ } , [ enableTilt ] ) ;
46135
47136 const setPointer = useCallback (
48137 ( event : PointerEvent < HTMLDivElement > ) => {
@@ -70,11 +159,7 @@ const ProfileCard = ({
70159 const node = ref . current ;
71160 if ( ! node ) return ;
72161
73- node . style . setProperty ( "--profile-x" , "50%" ) ;
74- node . style . setProperty ( "--profile-y" , "50%" ) ;
75- node . style . setProperty ( "--profile-rotate-x" , "0deg" ) ;
76- node . style . setProperty ( "--profile-rotate-y" , "0deg" ) ;
77- node . style . setProperty ( "--profile-glow-opacity" , "0" ) ;
162+ resetTilt ( node ) ;
78163 } , [ ] ) ;
79164
80165 const style = {
0 commit comments