The prompt that made it
as published# SCROLL EXPANSION HERO — COMPONENT INTEGRATION
YOU ARE GIVEN A TASK TO INTEGRATE AN EXISTING REACT COMPONENT IN THE CODEBASE
THE CODEBASE SHOULD SUPPORT:
- SHADCN PROJECT STRUCTURE
- TAILWIND CSS
- TYPESCRIPT
IF IT DOESN'T, PROVIDE INSTRUCTIONS ON HOW TO SETUP PROJECT VIA SHADCN CLI, INSTALL TAILWIND OR TYPESCRIPT.
DETERMINE THE DEFAULT PATH FOR COMPONENTS AND STYLES.
IF DEFAULT PATH FOR COMPONENTS IS NOT /COMPONENTS/UI, PROVIDE INSTRUCTIONS ON WHY IT'S IMPORTANT TO CREATE THIS FOLDER
COPY-PASTE THIS COMPONENT TO /COMPONENTS/UI FOLDER:
```tsx
scroll-expansion-hero.tsx
'use client';
import {
useEffect,
useRef,
useState,
ReactNode,
TouchEvent,
WheelEvent,
} from 'react';
import Image from 'next/image';
import { motion } from 'framer-motion';
interface ScrollExpandMediaProps {
mediaType?: 'video' | 'image';
mediaSrc: string;
posterSrc?: string;
bgImageSrc: string;
title?: string;
date?: string;
scrollToExpand?: string;
textBlend?: boolean;
children?: ReactNode;
}
const ScrollExpandMedia = ({
mediaType = 'video',
mediaSrc,
posterSrc,
bgImageSrc,
title,
date,
scrollToExpand,
textBlend,
children,
}: ScrollExpandMediaProps) => {
const [scrollProgress, setScrollProgress] = useState<number>(0);
const [showContent, setShowContent] = useState<boolean>(false);
const [mediaFullyExpanded, setMediaFullyExpanded] = useState<boolean>(false);
const [touchStartY, setTouchStartY] = useState<number>(0);
const [isMobileState, setIsMobileState] = useState<boolean>(false);
const sectionRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
setScrollProgress(0);
setShowContent(false);
setMediaFullyExpanded(false);
}, [mediaType]);
useEffect(() => {
const handleWheel = (e: WheelEvent) => {
if (mediaFullyExpanded && e.deltaY < 0 && window.scrollY <= 5) {
setMediaFullyExpanded(false);
e.preventDefault();
} else if (!mediaFullyExpanded) {
e.preventDefault();
const scrollDelta = e.deltaY * 0.0009;
const newProgress = Math.min(
Math.max(scrollProgress + scrollDelta, 0),
1
);
setScrollProgress(newProgress);
if (newProgress >= 1) {
setMediaFullyExpanded(true);
setShowContent(true);
} else if (newProgress < 0.75) {
setShowContent(false);
}
}
};
const handleTouchStart = (e: TouchEvent) => {
setTouchStartY(e.touches[0].clientY);
};
const handleTouchMove = (e: TouchEvent) => {
if (!touchStartY) return;
const touchY = e.touches[0].clientY;
const deltaY = touchStartY - touchY;
if (mediaFullyExpanded && deltaY < -20 && window.scrollY <= 5) {
setMediaFullyExpanded(false);
e.preventDefault();
} else if (!mediaFullyExpanded) {
e.preventDefault();
// Increase sensitivity for mobile, especially when scrolling back
const scrollFactor = deltaY < 0 ? 0.008 : 0.005; // Higher sensitivity for scrolling back
const scrollDelta = deltaY * scrollFactor;
const newProgress = Math.min(
Math.max(scrollProgress + scrollDelta, 0),
1
);
setScrollProgress(newProgress);
if (newProgress >= 1) {
setMediaFullyExpanded(true);
setShowContent(true);
} else if (newProgress < 0.75) {
setShowContent(false);
}
setTouchStartY(touchY);
}
};
const handleTouchEnd = (): void => {
setTouchStartY(0);
};
const handleScroll = (): void => {
if (!mediaFullyExpanded) {
window.scrollTo(0, 0);
}
};
window.addEventListener('wheel', handleWheel as unknown as EventListener, {
passive: false,
});
window.addEventListener('scroll', handleScroll as EventListener);
window.addEventListener(
'touchstart',
handleTouchStart as unknown as EventListener,
{ passive: false }
);
window.addEventListener(
'touchmove',
handleTouchMove as unknown as EventListener,
{ passive: false }
);
window.addEventListener('touchend', handleTouchEnd as EventListener);
return () => {
window.removeEventListener(
'wheel',
handleWheel as unknown as EventListener
);
window.removeEventListener('scroll', handleScroll as EventListener);
window.removeEventListener(
'touchstart',
handleTouchStart as unknown as EventListener
);
window.removeEventListener(
'touchmove',
handleTouchMove as unknown as EventListener
);
window.removeEventListener('touchend', handleTouchEnd as EventListener);
};
}, [scrollProgress, mediaFullyExpanded, touchStartY]);
useEffect(() => {
const checkIfMobile = (): void => {
setIsMobileState(window.innerWidth < 768);
};
checkIfMobile();
window.addEventListener('resize', checkIfMobile);
return () => window.removeEventListener('resize', checkIfMobile);
}, []);
const mediaWidth = 300 + scrollProgress * (isMobileState ? 650 : 1250);
const mediaHeight = 400 + scrollProgress * (isMobileState ? 200 : 400);
const textTranslateX = scrollProgress * (isMobileState ? 180 : 150);
const firstWord = title ? title.split(' ')[0] : '';
const restOfTitle = title ? title.split(' ').slice(1).join(' ') : '';
return (
<div
ref={sectionRef}
className='transition-colors duration-700 ease-in-out overflow-x-hidden'
>
<section className='relative flex flex-col items-center justify-start min-h-[100dvh]'>
<div className='relative w-full flex flex-col items-center min-h-[100dvh]'>
<motion.div
className='absolute inset-0 z-0 h-full'
initial={{ opacity: 0 }}
animate={{ opacity: 1 - scrollProgress }}
transition={{ duration: 0.1 }}
>
<Image
src={bgImageSrc}
alt='Background'
width={1920}
height={1080}
className='w-screen h-screen'
style={{
objectFit: 'cover',
objectPosition: 'center',
}}
priority
/>
<div className='absolute inset-0 bg-black/10' />
</motion.div>
<div className='container mx-auto flex flex-col items-center justify-start relative z-10'>
<div className='flex flex-col items-center justify-center w-full h-[100dvh] relative'>
<div
className='absolute z-0 top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 transition-none rounded-2xl'
style={{
width: `${mediaWidth}px`,
height: `${mediaHeight}px`,
maxWidth: '95vw',
maxHeight: '85vh',
boxShadow: '0px 0px 50px rgba(0, 0, 0, 0.3)',
}}
>
{mediaType === 'video' ? (
mediaSrc.includes('youtube.com') ? (
<div className='relative w-full h-full pointer-events-none'>
<iframe
width='100%'
height='100%'
src={
mediaSrc.includes('embed')
? mediaSrc +
(mediaSrc.includes('?') ? '&' : '?') +
'autoplay=1&mute=1&loop=1&controls=0&showinfo=0&rel=0&disablekb=1&modestbranding=1'
: mediaSrc.replace('watch?v=', 'embed/') +
'?autoplay=1&mute=1&loop=1&controls=0&showinfo=0&rel=0&disablekb=1&modestbranding=1&playlist=' +
mediaSrc.split('v=')[1]
}
className='w-full h-full rounded-xl'
frameBorder='0'
allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
allowFullScreen
/>
<div
className='absolute inset-0 z-10'
style={{ pointerEvents: 'none' }}
></div>
<motion.div
className='absolute inset-0 bg-black/30 rounded-xl'
initial={{ opacity: 0.7 }}
animate={{ opacity: 0.5 - scrollProgress * 0.3 }}
transition={{ duration: 0.2 }}
/>
</div>
) : (
<div className='relative w-full h-full pointer-events-none'>
<video
src={mediaSrc}
poster={posterSrc}
autoPlay
muted
loop
playsInline
preload='auto'
className='w-full h-full object-cover rounded-xl'
controls={false}
disablePictureInPicture
disableRemotePlayback
/>
<div
className='absolute inset-0 z-10'
style={{ pointerEvents: 'none' }}
></div>
<motion.div
className='absolute inset-0 bg-black/30 rounded-xl'
initial={{ opacity: 0.7 }}
animate={{ opacity: 0.5 - scrollProgress * 0.3 }}
transition={{ duration: 0.2 }}
/>
</div>
)
) : (
<div className='relative w-full h-full'>
<Image
src={mediaSrc}
alt={title || 'Media content'}
width={1280}
height={720}
className='w-full h-full object-cover rounded-xl'
/>
<motion.div
className='absolute inset-0 bg-black/50 rounded-xl'
initial={{ opacity: 0.7 }}
animate={{ opacity: 0.7 - scrollProgress * 0.3 }}
transition={{ duration: 0.2 }}
/>
</div>
)}
<div className='flex flex-col items-center text-center relative z-10 mt-4 transition-none'>
{date && (
<p
className='text-2xl text-blue-200'
style={{ transform: `translateX(-${textTranslateX}vw)` }}
>
{date}
</p>
)}
{scrollToExpand && (
<p
className='text-blue-200 font-medium text-center'
style={{ transform: `translateX(${textTranslateX}vw)` }}
>
{scrollToExpand}
</p>
)}
</div>
</div>
<div
className={`flex items-center justify-center text-center gap-4 w-full relative z-10 transition-none flex-col ${
textBlend ? 'mix-blend-difference' : 'mix-blend-normal'
}`}
>
<motion.h2
className='text-4xl md:text-5xl lg:text-6xl font-bold text-blue-200 transition-none'
style={{ transform: `translateX(-${textTranslateX}vw)` }}
>
{firstWord}
</motion.h2>
<motion.h2
className='text-4xl md:text-5xl lg:text-6xl font-bold text-center text-blue-200 transition-none'
style={{ transform: `translateX(${textTranslateX}vw)` }}
>
{restOfTitle}
</motion.h2>
</div>
</div>
<motion.section
className='flex flex-col w-full px-8 py-10 md:px-16 lg:py-20'
initial={{ opacity: 0 }}
animate={{ opacity: showContent ? 1 : 0 }}
transition={{ duration: 0.7 }}
>
{children}
</motion.section>
</div>
</div>
</section>
</div>
);
};
export default ScrollExpandMedia;
demo.tsx
'use client';
import { useState, useEffect } from 'react';
import ScrollExpandMedia from '@/components/blocks/scroll-expansion-hero';
interface MediaAbout {
overview: string;
conclusion: string;
}
interface MediaContent {
src: string;
poster?: string;
background: string;
title: string;
date: string;
scrollToExpand: string;
about: MediaAbout;
}
interface MediaContentCollection {
[key: string]: MediaContent;
}
const sampleMediaContent: MediaContentCollection = {
video: {
src: 'https://me7aitdbxq.ufs.sh/f/2wsMIGDMQRdYuZ5R8ahEEZ4aQK56LizRdfBSqeDMsmUIrJN1',
poster:
'https://images.pexels.com/videos/5752729/space-earth-universe-cosmos-5752729.jpeg',
background:
'https://me7aitdbxq.ufs.sh/f/2wsMIGDMQRdYMNjMlBUYHaeYpxduXPVNwf8mnFA61L7rkcoS',
title: 'Immersive Video Experience',
date: 'Cosmic Journey',
scrollToExpand: 'Scroll to Expand Demo',
about: {
overview:
'This is a demonstration of the ScrollExpandMedia component with a video. As you scroll, the video expands to fill more of the screen, creating an immersive experience. This component is perfect for showcasing video content in a modern, interactive way.',
conclusion:
'The ScrollExpandMedia component provides a unique way to engage users with your content through interactive scrolling. Try switching between video and image modes to see different implementations.',
},
},
image: {
src: 'https://images.unsplash.com/photo-1682687982501-1e58ab814714?q=80&w=1280&auto=format&fit=crop',
background:
'https://images.unsplash.com/photo-1506744038136-46273834b3fb?q=80&w=1920&auto=format&fit=crop',
title: 'Dynamic Image Showcase',
date: 'Underwater Adventure',
scrollToExpand: 'Scroll to Expand Demo',
about: {
overview:
'This is a demonstration of the ScrollExpandMedia component with an image. The same smooth expansion effect works beautifully with static images, allowing you to create engaging visual experiences without video content.',
conclusion:
'The ScrollExpandMedia component works equally well with images and videos. This flexibility allows you to choose the media type that best suits your content while maintaining the same engaging user experience.',
},
},
};
const MediaContent = ({ mediaType }: { mediaType: 'video' | 'image' }) => {
const currentMedia = sampleMediaContent[mediaType];
return (
<div className='max-w-4xl mx-auto'>
<h2 className='text-3xl font-bold mb-6 text-black dark:text-white'>
About This Component
</h2>
<p className='text-lg mb-8 text-black dark:text-white'>
{currentMedia.about.overview}
</p>
<p className='text-lg mb-8 text-black dark:text-white'>
{currentMedia.about.conclusion}
</p>
</div>
);
};
export const VideoExpansionTextBlend = () => {
const mediaType = 'video';
const currentMedia = sampleMediaContent[mediaType];
useEffect(() => {
window.scrollTo(0, 0);
const resetEvent = new Event('resetSection');
window.dispatchEvent(resetEvent);
}, []);
return (
<div className='min-h-screen'>
<ScrollExpandMedia
mediaType={mediaType}
mediaSrc={currentMedia.src}
posterSrc={currentMedia.poster}
bgImageSrc={currentMedia.background}
title={currentMedia.title}
date={currentMedia.date}
scrollToExpand={currentMedia.scrollToExpand}
textBlend
>
<MediaContent mediaType={mediaType} />
</ScrollExpandMedia>
</div>
);
};
export const ImageExpansionTextBlend = () => {
const mediaType = 'image';
const currentMedia = sampleMediaContent[mediaType];
useEffect(() => {
window.scrollTo(0, 0);
const resetEvent = new Event('resetSection');
window.dispatchEvent(resetEvent);
}, []);
return (
<div className='min-h-screen'>
<ScrollExpandMedia
mediaType={mediaType}
mediaSrc={currentMedia.src}
bgImageSrc={currentMedia.background}
title={currentMedia.title}
date={currentMedia.date}
scrollToExpand={currentMedia.scrollToExpand}
textBlend
>
<MediaContent mediaType={mediaType} />
</ScrollExpandMedia>
</div>
);
};
export const VideoExpansion = () => {
const mediaType = 'video';
const currentMedia = sampleMediaContent[mediaType];
useEffect(() => {
window.scrollTo(0, 0);
const resetEvent = new Event('resetSection');
window.dispatchEvent(resetEvent);
}, []);
return (
<div className='min-h-screen'>
<ScrollExpandMedia
mediaType={mediaType}
mediaSrc={currentMedia.src}
posterSrc={currentMedia.poster}
bgImageSrc={currentMedia.background}
title={currentMedia.title}
date={currentMedia.date}
scrollToExpand={currentMedia.scrollToExpand}
>
<MediaContent mediaType={mediaType} />
</ScrollExpandMedia>
</div>
);
};
export const ImageExpansion = () => {
const mediaType = 'image';
const currentMedia = sampleMediaContent[mediaType];
useEffect(() => {
window.scrollTo(0, 0);
const resetEvent = new Event('resetSection');
window.dispatchEvent(resetEvent);
}, []);
return (
<div className='min-h-screen'>
<ScrollExpandMedia
mediaType={mediaType}
mediaSrc={currentMedia.src}
bgImageSrc={currentMedia.background}
title={currentMedia.title}
date={currentMedia.date}
scrollToExpand={currentMedia.scrollToExpand}
>
<MediaContent mediaType={mediaType} />
</ScrollExpandMedia>
</div>
);
};
const Demo = () => {
const [mediaType, setMediaType] = useState('video');
const currentMedia = sampleMediaContent[mediaType];
useEffect(() => {
window.scrollTo(0, 0);
const resetEvent = new Event('resetSection');
window.dispatchEvent(resetEvent);
}, [mediaType]);
return (
<div className='min-h-screen'>
<div className='fixed top-4 right-4 z-50 flex gap-2'>
<button
onClick={() => setMediaType('video')}
className={`px-4 py-2 rounded-lg ${
mediaType === 'video'
? 'bg-white text-black'
: 'bg-black/50 text-white border border-white/30'
}`}
>
Video
</button>
<button
onClick={() => setMediaType('image')}
className={`px-4 py-2 rounded-lg ${
mediaType === 'image'
? 'bg-white text-black'
: 'bg-black/50 text-white border border-white/30'
}`}
>
Image
</button>
</div>
<ScrollExpandMedia
mediaType={mediaType as 'video' | 'image'}
mediaSrc={currentMedia.src}
posterSrc={mediaType === 'video' ? currentMedia.poster : undefined}
bgImageSrc={currentMedia.background}
title={currentMedia.title}
date={currentMedia.date}
scrollToExpand={currentMedia.scrollToExpand}
>
<MediaContent mediaType={mediaType as 'video' | 'image'} />
</ScrollExpandMedia>
</div>
);
};
export default Demo;
```
INSTALL NPM DEPENDENCIES:
```bash
framer-motion
```
## IMPLEMENTATION GUIDELINES
1. ANALYZE THE COMPONENT STRUCTURE AND IDENTIFY ALL REQUIRED DEPENDENCIES
2. REVIEW THE COMPONENT'S ARGUMENS AND STATE
3. IDENTIFY ANY REQUIRED CONTEXT PROVIDERS OR HOOKS AND INSTALL THEM
4. QUESTIONS TO ASK
- WHAT DATA/PROPS WILL BE PASSED TO THIS COMPONENT?
- ARE THERE ANY SPECIFIC STATE MANAGEMENT REQUIREMENTS?
- ARE THERE ANY REQUIRED ASSETS (IMAGES, ICONS, ETC.)?
- WHAT IS THE EXPECTED RESPONSIVE BEHAVIOR?
- WHAT IS THE BEST PLACE TO USE THIS COMPONENT IN THE APP?
## STEPS TO INTEGRATE
0. COPY PASTE ALL THE CODE ABOVE IN THE CORRECT DIRECTORIES
1. INSTALL EXTERNAL DEPENDENCIES
2. FILL IMAGE ASSETS WITH UNSPLASH STOCK IMAGES YOU KNOW EXIST
3. USE LUCIDE-REACT ICONS FOR SVGS OR LOGOS IF COMPONENT REQUIRES THEMAdds a render step. Works best with the HyperFrames or Remotion skills installed. Free, no sign-up.
Prompt by @pulkitxm. Prompts belong to their creators.