636 lines
18 KiB
TypeScript

import React from "react";
import {
AbsoluteFill,
Img,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
Series,
staticFile,
} from "remotion";
// ============================================
// SPRING CONFIGS (from best practices)
// ============================================
const SPRING_SMOOTH = { damping: 200 };
const SPRING_SNAPPY = { damping: 20, stiffness: 200 };
const SPRING_BOUNCY = { damping: 12 };
// ============================================
// CAMERA - Simple wrapper, ONE motion at a time
// ============================================
const Camera: React.FC<{
children: React.ReactNode;
zoom?: number;
x?: number;
y?: number;
}> = ({ children, zoom = 1, x = 0, y = 0 }) => (
<div
style={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
transform: `scale(${zoom}) translate(${x}px, ${y}px)`,
transformOrigin: "center center",
}}
>
{children}
</div>
);
// ============================================
// KINETIC TEXT - Word by word reveal
// ============================================
const KineticText: React.FC<{
text: string;
delay?: number;
stagger?: number;
style?: React.CSSProperties;
}> = ({ text, delay = 0, stagger = 4, style = {} }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const words = text.split(" ");
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.3em", ...style }}>
{words.map((word, i) => {
const wordDelay = delay + i * stagger;
const progress = spring({
frame: frame - wordDelay,
fps,
config: SPRING_SNAPPY,
});
return (
<span
key={i}
style={{
display: "inline-block",
transform: `translateY(${interpolate(progress, [0, 1], [40, 0])}px)`,
opacity: progress,
}}
>
{word}
</span>
);
})}
</div>
);
};
// ============================================
// BROWSER MOCKUP
// ============================================
const BrowserMockup: React.FC<{
src: string;
width?: number;
}> = ({ src, width = 1000 }) => {
const height = width * 0.5625;
return (
<div
style={{
backgroundColor: "#1e293b",
borderRadius: 12,
overflow: "hidden",
boxShadow: "0 25px 60px rgba(0,0,0,0.4)",
}}
>
<div
style={{
height: 36,
backgroundColor: "#0f172a",
display: "flex",
alignItems: "center",
padding: "0 14px",
gap: 7,
}}
>
<div style={{ width: 11, height: 11, borderRadius: "50%", backgroundColor: "#ef4444" }} />
<div style={{ width: 11, height: 11, borderRadius: "50%", backgroundColor: "#f59e0b" }} />
<div style={{ width: 11, height: 11, borderRadius: "50%", backgroundColor: "#22c55e" }} />
<div
style={{
flex: 1,
marginLeft: 14,
height: 22,
backgroundColor: "#1e293b",
borderRadius: 5,
paddingLeft: 10,
fontSize: 11,
color: "#64748b",
display: "flex",
alignItems: "center",
}}
>
app.reonomy.com
</div>
</div>
<Img src={src} style={{ width, height, objectFit: "cover" }} />
</div>
);
};
// ============================================
// STEP LABEL
// ============================================
const StepLabel: React.FC<{
step: number;
text: string;
color?: string;
}> = ({ step, text, color = "#6366f1" }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({ frame: frame - 10, fps, config: SPRING_SNAPPY });
return (
<div
style={{
position: "absolute",
bottom: 50,
left: 50,
transform: `translateY(${interpolate(progress, [0, 1], [30, 0])}px)`,
opacity: progress,
}}
>
<div
style={{
background: `linear-gradient(135deg, ${color}, ${color}dd)`,
padding: "14px 28px",
borderRadius: 10,
boxShadow: `0 10px 30px ${color}40`,
}}
>
<span style={{ fontSize: 20, fontWeight: 700, color: "white" }}>
Step {step}: {text}
</span>
</div>
</div>
);
};
// ============================================
// CONTACT CARD
// ============================================
const ContactCard: React.FC<{
type: "phone" | "email";
value: string;
source?: string;
delay: number;
}> = ({ type, value, source, delay }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({ frame: frame - delay, fps, config: SPRING_SNAPPY });
return (
<div
style={{
display: "flex",
alignItems: "center",
gap: 14,
padding: "12px 16px",
backgroundColor: "rgba(255,255,255,0.95)",
borderRadius: 10,
marginBottom: 8,
boxShadow: "0 4px 20px rgba(0,0,0,0.1)",
transform: `translateY(${interpolate(progress, [0, 1], [20, 0])}px)`,
opacity: progress,
}}
>
<div
style={{
width: 40,
height: 40,
borderRadius: 8,
background:
type === "phone"
? "linear-gradient(135deg, #10b981, #059669)"
: "linear-gradient(135deg, #6366f1, #4f46e5)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 18,
}}
>
{type === "phone" ? "📞" : "📧"}
</div>
<div>
<div style={{ fontSize: 14, fontWeight: 600, color: "#1e293b" }}>{value}</div>
{source && <div style={{ fontSize: 11, color: "#64748b" }}>{source}</div>}
</div>
</div>
);
};
// ============================================
// ANIMATED STAT
// ============================================
const AnimatedStat: React.FC<{
value: number | string;
label: string;
color: string;
delay: number;
}> = ({ value, label, color, delay }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({ frame: frame - delay, fps, config: SPRING_SMOOTH });
const numericValue =
typeof value === "number" ? Math.round(interpolate(progress, [0, 1], [0, value])) : value;
return (
<div
style={{
textAlign: "center",
transform: `scale(${interpolate(progress, [0, 1], [0.8, 1])})`,
opacity: progress,
}}
>
<div style={{ fontSize: 48, fontWeight: 800, color }}>{numericValue}</div>
<div style={{ fontSize: 14, color: "#94a3b8", marginTop: 4 }}>{label}</div>
</div>
);
};
// ============================================
// SCENES
// ============================================
const SceneIntro: React.FC = () => {
const frame = useCurrentFrame();
// Slow, elegant zoom settle: 1.12 -> 1.0 over longer duration
const zoom = interpolate(frame, [0, 120], [1.12, 1.0], {
extrapolateRight: "clamp",
});
return (
<AbsoluteFill>
<Camera zoom={zoom}>
<div style={{ textAlign: "center" }}>
<KineticText
text="🏢 Reonomy Contact Extractor"
delay={20}
stagger={6}
style={{ fontSize: 64, fontWeight: 800, color: "white", justifyContent: "center" }}
/>
<div style={{ marginTop: 20 }}>
<KineticText
text="CRE Leads → Your CRM … On Demand"
delay={70}
stagger={5}
style={{ fontSize: 28, fontWeight: 500, color: "#94a3b8", justifyContent: "center" }}
/>
</div>
</div>
</Camera>
</AbsoluteFill>
);
};
const SceneLogin: React.FC = () => {
const frame = useCurrentFrame();
// Slow push in toward login area: 0.92 -> 1.08 over 140 frames
const zoom = interpolate(frame, [0, 140], [0.92, 1.08], {
extrapolateRight: "clamp",
});
return (
<AbsoluteFill>
<Camera zoom={zoom}>
<BrowserMockup src={staticFile("assets/02-login-filled.png")} width={1000} />
</Camera>
<StepLabel step={1} text="Login" />
</AbsoluteFill>
);
};
const SceneSearch: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Slower settle: start wide, ease in over 100 frames
const zoom = interpolate(frame, [0, 100], [0.88, 1.0], {
extrapolateRight: "clamp",
});
const filterProgress1 = spring({ frame: frame - 50, fps, config: SPRING_SNAPPY });
const filterProgress2 = spring({ frame: frame - 70, fps, config: SPRING_SNAPPY });
return (
<AbsoluteFill>
<Camera zoom={zoom}>
<div style={{ position: "relative" }}>
<BrowserMockup src={staticFile("assets/04-search-results.png")} width={1050} />
{/* Filter badges - staggered */}
<div
style={{
position: "absolute",
left: -160,
top: 140,
}}
>
<div
style={{
background: "rgba(16, 185, 129, 0.9)",
padding: "10px 18px",
borderRadius: 20,
marginBottom: 10,
boxShadow: "0 4px 20px rgba(16, 185, 129, 0.3)",
opacity: filterProgress1,
transform: `translateX(${interpolate(filterProgress1, [0, 1], [-30, 0])}px)`,
}}
>
<span style={{ color: "white", fontWeight: 600, fontSize: 14 }}> Has Phone</span>
</div>
<div
style={{
background: "rgba(99, 102, 241, 0.9)",
padding: "10px 18px",
borderRadius: 20,
boxShadow: "0 4px 20px rgba(99, 102, 241, 0.3)",
opacity: filterProgress2,
transform: `translateX(${interpolate(filterProgress2, [0, 1], [-30, 0])}px)`,
}}
>
<span style={{ color: "white", fontWeight: 600, fontSize: 14 }}> Has Email</span>
</div>
</div>
</div>
</Camera>
<StepLabel step={2} text="Filtered Search" />
</AbsoluteFill>
);
};
const SceneProperty: React.FC = () => {
const frame = useCurrentFrame();
// Slow, cinematic pan down over 120 frames
const y = interpolate(frame, [0, 120], [-25, 15], {
extrapolateRight: "clamp",
});
return (
<AbsoluteFill>
<Camera y={y}>
<BrowserMockup src={staticFile("assets/05-property-detail.png")} width={1000} />
</Camera>
<StepLabel step={3} text="Property Details" />
</AbsoluteFill>
);
};
const SceneOwner: React.FC = () => {
const frame = useCurrentFrame();
// Slow zoom in to owner section over 120 frames
const zoom = interpolate(frame, [0, 120], [1.0, 1.12], {
extrapolateRight: "clamp",
});
return (
<AbsoluteFill>
<Camera zoom={zoom}>
<BrowserMockup src={staticFile("assets/06-owner-tab.png")} width={1000} />
</Camera>
<StepLabel step={4} text="Owner Tab" color="#8b5cf6" />
</AbsoluteFill>
);
};
const SceneModal: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Slower pop in then settle over longer duration
const zoom = interpolate(frame, [0, 50, 100], [0.85, 1.04, 1.0], {
extrapolateRight: "clamp",
});
const labelProgress = spring({ frame: frame - 70, fps, config: SPRING_BOUNCY });
return (
<AbsoluteFill>
<Camera zoom={zoom}>
<BrowserMockup src={staticFile("assets/09-contact-modal.png")} width={1050} />
</Camera>
{/* Success label */}
<div
style={{
position: "absolute",
bottom: 50,
left: "50%",
transform: `translateX(-50%) translateY(${interpolate(labelProgress, [0, 1], [40, 0])}px)`,
opacity: labelProgress,
}}
>
<div
style={{
background: "linear-gradient(135deg, #10b981, #059669)",
padding: "16px 40px",
borderRadius: 12,
boxShadow: "0 15px 40px rgba(16, 185, 129, 0.4)",
}}
>
<span style={{ fontSize: 24, fontWeight: 700, color: "white" }}>
🎉 Contacts Extracted!
</span>
</div>
</div>
</AbsoluteFill>
);
};
const SceneResults: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Slow, elegant zoom out over 150 frames
const zoom = interpolate(frame, [0, 150], [1.06, 1.0], {
extrapolateRight: "clamp",
});
const phones = [
{ value: "919-469-9553", source: "Greystone Property Mgmt" },
{ value: "727-341-0186", source: "Seaside Villas" },
{ value: "903-566-9506", source: "Apartment Income Reit" },
{ value: "407-671-2400", source: "E R Management" },
{ value: "407-382-2683", source: "Bellagio Apartments" },
];
const emails = [
{ value: "berrizoro@gmail.com" },
{ value: "aberriz@hotmail.com" },
{ value: "jasonhitch1@gmail.com" },
{ value: "albert@annarborusa.org" },
{ value: "albertb@sterlinghousing.com" },
];
const headerProgress = spring({ frame: frame - 10, fps, config: SPRING_SMOOTH });
const phoneHeaderProgress = spring({ frame: frame - 60, fps, config: SPRING_SMOOTH });
const emailHeaderProgress = spring({ frame: frame - 70, fps, config: SPRING_SMOOTH });
return (
<AbsoluteFill>
<Camera zoom={zoom}>
<div style={{ width: 1600, padding: 40 }}>
{/* Title */}
<div style={{ textAlign: "center", marginBottom: 30 }}>
<div
style={{
opacity: headerProgress,
transform: `translateY(${interpolate(headerProgress, [0, 1], [20, 0])}px)`,
}}
>
<span style={{ fontSize: 42, fontWeight: 800, color: "white" }}>
CRE Leads Delivered Direct to Your CRM
</span>
</div>
<div
style={{
marginTop: 10,
opacity: spring({ frame: frame - 25, fps, config: SPRING_SMOOTH }),
}}
>
<span
style={{
fontSize: 24,
fontWeight: 600,
background: "linear-gradient(90deg, #6366f1, #a855f7, #ec4899)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}}
>
On Demand 🚀
</span>
</div>
</div>
{/* Contact columns */}
<div style={{ display: "flex", gap: 40, paddingLeft: 80, paddingRight: 80 }}>
<div style={{ flex: 1 }}>
<div
style={{
fontSize: 18,
color: "#10b981",
marginBottom: 12,
fontWeight: 700,
opacity: phoneHeaderProgress,
}}
>
📞 Phone Numbers
</div>
{phones.map((p, i) => (
<ContactCard
key={p.value}
type="phone"
value={p.value}
source={p.source}
delay={80 + i * 15}
/>
))}
</div>
<div style={{ flex: 1 }}>
<div
style={{
fontSize: 18,
color: "#6366f1",
marginBottom: 12,
fontWeight: 700,
opacity: emailHeaderProgress,
}}
>
📧 Email Addresses
</div>
{emails.map((e, i) => (
<ContactCard key={e.value} type="email" value={e.value} delay={90 + i * 15} />
))}
</div>
</div>
{/* Stats footer */}
<div
style={{
marginTop: 30,
display: "flex",
justifyContent: "space-around",
background: "rgba(255,255,255,0.05)",
backdropFilter: "blur(10px)",
padding: "20px 50px",
borderRadius: 14,
marginLeft: 80,
marginRight: 80,
}}
>
<AnimatedStat value={10} label="Total Contacts" color="#6366f1" delay={200} />
<AnimatedStat value={5} label="Phone Numbers" color="#10b981" delay={220} />
<AnimatedStat value={5} label="Emails" color="#f59e0b" delay={240} />
<AnimatedStat value="~60s" label="Extraction Time" color="#ef4444" delay={260} />
</div>
</div>
</Camera>
</AbsoluteFill>
);
};
// ============================================
// MAIN COMPOSITION
// ============================================
export const ReonomyDemo: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
// Global fade in/out (20 frames as recommended)
const fadeIn = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: "clamp" });
const fadeOut = interpolate(frame, [durationInFrames - 20, durationInFrames], [1, 0], {
extrapolateLeft: "clamp",
});
return (
<AbsoluteFill
style={{
background: "linear-gradient(135deg, #0f172a 0%, #1e1b4b 50%, #0f172a 100%)",
opacity: Math.min(fadeIn, fadeOut),
}}
>
<Series>
<Series.Sequence durationInFrames={210}>
<SceneIntro />
</Series.Sequence>
<Series.Sequence durationInFrames={180}>
<SceneLogin />
</Series.Sequence>
<Series.Sequence durationInFrames={210}>
<SceneSearch />
</Series.Sequence>
<Series.Sequence durationInFrames={180}>
<SceneProperty />
</Series.Sequence>
<Series.Sequence durationInFrames={180}>
<SceneOwner />
</Series.Sequence>
<Series.Sequence durationInFrames={210}>
<SceneModal />
</Series.Sequence>
<Series.Sequence durationInFrames={420}>
<SceneResults />
</Series.Sequence>
</Series>
</AbsoluteFill>
);
};