← Catalog
Edit Props (live)
TEXT
TEXT
TEXT
IMAGE
Source
248 lines// Port of the hyperframes `instagram-follow` block (Apache-2.0).
// Original: github.com/heygen-com/hyperframes @ registry/blocks/instagram-follow
// Rewritten in Remotion: GSAP timeline → interpolate() + spring().
//
// Timeline (at 30fps):
// 0.1s — card slides up from bottom (y: 300 → 0, opacity 0 → 1)
// 1.0s — follow button press-in (scale 0.92)
// 1.15s — elastic spring back + IG blue → dark gray + Follow fades out
// 1.18s — Following + chevron fades in
// 3.8s — card slides back down to bottom
// 4.5s — done
import React from 'react';
import {
AbsoluteFill,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
interface Props {
displayName: string;
handle: string;
followerCount: string;
avatarUrl: string;
}
export default function InstagramFollow({
displayName,
handle,
followerCount,
avatarUrl,
}: Props) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const s = (seconds: number) => seconds * fps;
const slideIn = interpolate(frame, [s(0.1), s(0.6)], [300, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const fadeIn = interpolate(frame, [s(0.1), s(0.6)], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const slideOut = interpolate(frame, [s(3.8), s(4.05)], [0, 300], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const fadeOut = interpolate(frame, [s(3.8), s(4.05)], [1, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
// Button press-in then elastic release
const pressIn = interpolate(frame, [s(1.0), s(1.15)], [1, 0.92], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const bounce =
frame >= s(1.15)
? spring({ frame: frame - s(1.15), fps, config: { damping: 8, stiffness: 120 } })
: 0;
const btnScale = frame < s(1.15) ? pressIn : 0.92 + bounce * 0.08;
// Blue → dark gray + Follow/Following crossfade at 1.15s
const colorT = interpolate(frame, [s(1.15), s(1.27)], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const btnBg = lerpColor('#0095f6', '#2f2f2f', colorT);
const followOpacity = interpolate(frame, [s(1.15), s(1.23)], [1, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const followingOpacity = interpolate(frame, [s(1.18), s(1.26)], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const translateY = slideIn + slideOut;
const opacity = Math.min(fadeIn, fadeOut);
return (
<AbsoluteFill
style={{
background: 'linear-gradient(180deg, #2a1a3e 0%, #0d1021 100%)',
fontFamily: 'system-ui, sans-serif',
}}
>
<div
style={{
position: 'absolute',
bottom: 240,
left: '50%',
transform: `translate(-50%, ${translateY}px)`,
opacity,
display: 'flex',
alignItems: 'center',
gap: 30,
background: '#1a1a1a',
borderRadius: 75,
padding: '25px 40px 25px 25px',
boxShadow: '0 8px 40px rgba(0, 0, 0, 0.4)',
}}
>
<Avatar url={avatarUrl} />
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 2,
marginRight: 20,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div
style={{
fontSize: 42,
fontWeight: 700,
color: '#fff',
lineHeight: 1.3,
}}
>
{displayName}
</div>
<VerifiedBadge />
</div>
<div style={{ fontSize: 28, color: '#a0a0a0', lineHeight: 1.3 }}>
{handle}
</div>
<div style={{ fontSize: 25, color: '#737373', lineHeight: 1.3 }}>
{followerCount}
</div>
</div>
<div
style={{
position: 'relative',
width: 250,
height: 80,
borderRadius: 40,
background: btnBg,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
transform: `scale(${btnScale})`,
}}
>
<span
style={{
position: 'absolute',
fontSize: 30,
fontWeight: 700,
color: '#fff',
opacity: followOpacity,
}}
>
Follow
</span>
<span
style={{
position: 'absolute',
fontSize: 30,
fontWeight: 700,
color: '#fff',
opacity: followingOpacity,
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
Following
<svg viewBox='0 0 24 24' width='18' height='18' fill='none'>
<polyline
points='6 9 12 15 18 9'
stroke='#fff'
strokeWidth={3}
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
</span>
</div>
</div>
</AbsoluteFill>
);
}
function Avatar({ url }: { url: string }) {
if (url) {
return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={url}
alt=''
style={{
width: 120,
height: 120,
borderRadius: '50%',
objectFit: 'cover',
flexShrink: 0,
border: '3px solid #333',
}}
/>
);
}
return (
<div
style={{
width: 120,
height: 120,
borderRadius: '50%',
flexShrink: 0,
border: '3px solid #333',
background:
'linear-gradient(135deg, #833ab4 0%, #fd1d1d 50%, #fcb045 100%)',
}}
/>
);
}
function VerifiedBadge() {
return (
<svg viewBox='0 0 40 40' width='34' height='34' fill='none'>
<circle cx='20' cy='20' r='20' fill='#0095F6' />
<path
d='M17.5 27.5L10 20l2.5-2.5 5 5 10-10L30 15 17.5 27.5z'
fill='#fff'
/>
</svg>
);
}
function lerpColor(a: string, b: string, t: number): string {
const ca = hexToRgb(a);
const cb = hexToRgb(b);
return `rgb(${Math.round(ca[0] + (cb[0] - ca[0]) * t)}, ${Math.round(
ca[1] + (cb[1] - ca[1]) * t
)}, ${Math.round(ca[2] + (cb[2] - ca[2]) * t)})`;
}
function hexToRgb(hex: string): [number, number, number] {
const m = hex.replace('#', '');
const n = parseInt(m, 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
Schema (Zod)
import { z } from 'zod';
export const Schema = z.object({
displayName: z.string().describe('$port:text Display name'),
handle: z.string().describe('$port:text Handle (e.g. @wireflow_ai)'),
followerCount: z.string().describe('$port:text Follower count label'),
avatarUrl: z.string().describe('$port:image Avatar image URL').default(''),
});
Instagram Follow
instagram_followInstagram-style profile card with verified badge and Follow → Following toggle animation. Slide-in, elastic button press, slide-out. Ported from hyperframes (Apache-2.0).
Workflow Inputs (4)
- Display name
displayNameTEXTRequired - Handle (e.g. @wireflow_ai)
handleTEXTRequired - Follower count label (e.g. "12.4K followers")
followerCountTEXTRequired - Avatar image URL
avatarUrlIMAGE
Meta
- Updated
- 4/21/2026