Skip to content

Commit

Permalink
feat: add tilt component (#82)
Browse files Browse the repository at this point in the history
* feat: add Tiltable component with examples and documentation

* docs: add usage examples to illustrate the Tilt component's features

* feat: add isRevese prop to Tilt component for reverse rotation

* docs: add isRevese prop description to Tilt component documentation

* feat: update tilt API

* feat: add tilt-spotlight

* feat: tilt UI examples update

* fix: build

---------

Co-authored-by: afpedreros <[email protected]>
  • Loading branch information
ibelick and AFPedreros committed Nov 25, 2024
1 parent 5f72b88 commit fa4d98d
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 0 deletions.
5 changes: 5 additions & 0 deletions app/docs/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ const NAVIGATION: NavigationGroup[] = [
name: 'Spinning Text',
href: '/docs/spinning-text',
},
{
name: 'Tilt',
href: '/docs/tilt',
isNew: true,
},
],
},
];
Expand Down
52 changes: 52 additions & 0 deletions app/docs/tilt/page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export const metadata = {
title: 'Tilt - Motion-Primitives',
description:
'3D tilt effect that responds to mouse movement, enhancing UI elements with a dynamic depth effect, customizable rotation factors and spring options.',
};

import { TiltCard1 } from './tilt-card-1';
import { TiltSpotlight } from './tilt-spotlight';
import ComponentCodePreview from '@/components/website/component-code-preview';
import CodeBlock from '@/components/website/code-block';

# Tilt

3D tilt effect that responds to mouse movement, enhancing UI elements with a dynamic depth effect, customizable rotation factors and spring options.

## Examples

### Basic Tilt Card

<ComponentCodePreview
component={<TiltCard1 />}
filePath='app/docs/tilt/tilt-card-1.tsx'
/>

### Tilt with Spotlight

Example of the [Spotlight component](/docs/spotlight) used with the Tilt component.

<ComponentCodePreview
component={<TiltSpotlight />}
filePath='app/docs/tilt/tilt-spotlight.tsx'
/>

## Code

<CodeBlock filePath='components/core/tilt.tsx' />

## Component API

### Border Trail

| Prop | Type | Default | Description |
| :------------- | :------------ | :------ | :---------------------------------------------- |
| className | string | | Additional CSS classes for styling the tilt. |
| style | MotionStyle | | Additional CSS classes for styling the tilt. |
| rotationFactor | number | 15 | Controls the maximum rotation angle in degrees. |
| isRevese | boolean | false | Reverses the tilt effect's rotation direction. |
| springOptions | SpringOptions | | Spring options for the tilt effect. |

## Credits

Initiated by [@AFPedreros](https://github.com/AFPedreros)
26 changes: 26 additions & 0 deletions app/docs/tilt/tilt-card-1.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Tilt } from '@/components/core/tilt';

export function TiltCard1() {
return (
<Tilt rotationFactor={8} isRevese>
<div
style={{
borderRadius: '12px',
}}
className='flex max-w-[270px] flex-col overflow-hidden border border-zinc-950/10 bg-white dark:border-zinc-50/10 dark:bg-zinc-900'
>
<img
src='https://images.beta.cosmos.so/f7fcb95d-981b-4cb3-897f-e35f6c20e830?format=jpeg'
alt='Ghost in the shell - Kôkaku kidôtai'
className='h-48 w-full object-cover'
/>
<div className='p-2'>
<h1 className='font-mono leading-snug text-zinc-950 dark:text-zinc-50'>
Ghost in the Shell
</h1>
<p className='text-zinc-700 dark:text-zinc-400'>Kôkaku kidôtai</p>
</div>
</div>
</Tilt>
);
}
43 changes: 43 additions & 0 deletions app/docs/tilt/tilt-spotlight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Spotlight } from '@/components/core/spotlight';
import { Tilt } from '@/components/core/tilt';

export function TiltSpotlight() {
return (
<div className='max-w-sm'>
<Tilt
rotationFactor={6}
isRevese
style={{
transformOrigin: 'center center',
}}
springOptions={{
stiffness: 26.7,
damping: 4.1,
mass: 0.2,
}}
className='group relative rounded-lg'
>
<Spotlight
className='z-10 from-white/50 via-white/20 to-white/10 blur-2xl'
size={248}
springOptions={{
stiffness: 26.7,
damping: 4.1,
mass: 0.2,
}}
/>
<img
src='https://images.beta.cosmos.so/f7fcb95d-981b-4cb3-897f-e35f6c20e830?format=jpeg'
alt='Ghost in the shell - Kôkaku kidôtai'
className='h-32 w-full rounded-lg object-cover grayscale duration-700 group-hover:grayscale-0'
/>
</Tilt>
<div className='flex flex-col space-y-0.5 pb-0 pt-3'>
<h3 className='font-mono text-sm font-medium text-zinc-500 dark:text-zinc-400'>
Ghost in the Shell
</h3>
<p className='text-sm text-black dark:text-white'>Kôkaku kidôtai</p>
</div>
</div>
);
}
92 changes: 92 additions & 0 deletions components/core/tilt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
'use client';

import React, { useRef } from 'react';
import {
motion,
useMotionTemplate,
useMotionValue,
useSpring,
useTransform,
MotionStyle,
SpringOptions,
} from 'framer-motion';

type TiltProps = {
children: React.ReactNode;
className?: string;
style?: MotionStyle;
rotationFactor?: number;
isRevese?: boolean;
springOptions?: SpringOptions;
};

export function Tilt({
children,
className,
style,
rotationFactor = 15,
isRevese = false,
springOptions,
}: TiltProps) {
const ref = useRef<HTMLDivElement>(null);

const x = useMotionValue(0);
const y = useMotionValue(0);

const xSpring = useSpring(x, springOptions);
const ySpring = useSpring(y, springOptions);

const rotateX = useTransform(
ySpring,
[-0.5, 0.5],
isRevese
? [rotationFactor, -rotationFactor]
: [-rotationFactor, rotationFactor]
);
const rotateY = useTransform(
xSpring,
[-0.5, 0.5],
isRevese
? [-rotationFactor, rotationFactor]
: [rotationFactor, -rotationFactor]
);

const transform = useMotionTemplate`perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;

const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!ref.current) return;

const rect = ref.current.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;

const xPos = mouseX / width - 0.5;
const yPos = mouseY / height - 0.5;

x.set(xPos);
y.set(yPos);
};

const handleMouseLeave = () => {
x.set(0);
y.set(0);
};

return (
<motion.div
ref={ref}
className={className}
style={{
transformStyle: 'preserve-3d',
...style,
transform,
}}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
{children}
</motion.div>
);
}

0 comments on commit fa4d98d

Please sign in to comment.