Catalog

Edit Props (live)

config
config

Source

164 lines
import React from "react";
import {
  AbsoluteFill,
  useCurrentFrame,
  useVideoConfig,
  interpolate,
  spring,
} from "remotion";
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";

loadInter("normal", { weights: ["400", "500", "700", "900"] });

const FONT = "'Inter', sans-serif";

interface Card {
  title: string;
  body: string;
  reveal_at: number; // seconds, scene-relative
  accent?: string;   // optional left-edge accent colour
}

interface FeatureCardsProps {
  cards: Card[];
  backgroundColor?: string;
}

// Hex → rgba helper for accent glow
const hex2rgba = (hex: string, a: number) => {
  const h = hex.replace("#", "");
  const r = parseInt(h.slice(0, 2), 16);
  const g = parseInt(h.slice(2, 4), 16);
  const b = parseInt(h.slice(4, 6), 16);
  return `rgba(${r},${g},${b},${a})`;
};

export const FeatureCards: React.FC<FeatureCardsProps> = ({
  cards,
  backgroundColor = "#0c0d12",
}) => {
  const frame = useCurrentFrame();
  const { fps, width, height } = useVideoConfig();

  const N = cards.length;
  const gap = 24;
  // Live in the top 2/3 of the 1920 frame — that's where eyes focus.
  // Top edge ~80px, stack ends ~1280px → cards occupy ~1200px vertical.
  const stackTop = 80;
  const stackBottom = Math.floor(height * 0.66); // 66% of frame
  const stackH = stackBottom - stackTop;
  const cardH = Math.floor((stackH - gap * (N - 1)) / N);

  return (
    <AbsoluteFill style={{
      background: `linear-gradient(180deg, #0e0f15 0%, #0a0b10 100%)`,
    }}>
      <div style={{
        position: "absolute",
        top: stackTop,
        left: 36,
        right: 36,
        display: "flex",
        flexDirection: "column",
        gap,
      }}>
      {cards.map((c, i) => {
        // spring-based slide + fade
        const sp = spring({
          frame: Math.max(0, frame - Math.floor(c.reveal_at * fps)),
          fps,
          config: { mass: 0.55, damping: 14, stiffness: 130 },
          durationInFrames: 22,
        });
        const enterX = interpolate(sp, [0, 1], [width + 80, 0]);
        const op = interpolate(sp, [0, 1], [0, 1]);
        const t = frame / fps;
        if (t < c.reveal_at) return null;

        const accent = c.accent || "#9aa3b2";

        return (
          <div key={i} style={{
            position: "relative",
            // Subtle inner gradient for depth — feels glassy not flat
            background: "linear-gradient(180deg, rgba(28,30,38,0.92) 0%, rgba(20,22,28,0.92) 100%)",
            backdropFilter: "blur(8px)",
            border: "1px solid rgba(255,255,255,0.06)",
            borderRadius: 24,
            padding: "26px 32px",
            height: cardH,
            display: "flex",
            flexDirection: "column",
            justifyContent: "center",
            transform: `translateX(${enterX}px)`,
            opacity: op,
            boxShadow: `0 24px 60px rgba(0,0,0,0.55), 0 1px 0 rgba(255,255,255,0.04) inset`,
            overflow: "hidden",
          }}>
            {/* Soft accent glow — top-left wash, very subtle */}
            <div style={{
              position: "absolute",
              top: -120, left: -60,
              width: 320, height: 320,
              background: `radial-gradient(circle, ${hex2rgba(accent, 0.18)} 0%, transparent 65%)`,
              pointerEvents: "none",
              filter: "blur(20px)",
            }} />

            {/* Top row — accent dot + kicker */}
            <div style={{
              display: "flex",
              alignItems: "center",
              gap: 14,
              marginBottom: 18,
            }}>
              <div style={{
                width: 10, height: 10,
                borderRadius: "50%",
                background: accent,
                boxShadow: `0 0 12px ${hex2rgba(accent, 0.7)}`,
              }} />
              <div style={{
                fontFamily: FONT,
                fontSize: 20,
                fontWeight: 500,
                color: "rgba(230,230,240,0.55)",
                letterSpacing: 2.4,
                textTransform: "uppercase",
              }}>
                Skill
              </div>
            </div>

            {/* Title — restrained weight, cleaner letterspacing */}
            <div style={{
              fontFamily: FONT,
              fontSize: 50,
              fontWeight: 700,
              color: "#f4f5f8",
              letterSpacing: -1.4,
              lineHeight: 1.05,
              marginBottom: 14,
            }}>
              {c.title}
            </div>

            {/* Body — lighter weight, subtler colour, readable */}
            <div style={{
              fontFamily: FONT,
              fontSize: 26,
              fontWeight: 400,
              color: "rgba(210,212,222,0.72)",
              lineHeight: 1.35,
              letterSpacing: -0.2,
            }}>
              {c.body}
            </div>
          </div>
        );
      })}
      </div>
    </AbsoluteFill>
  );
};

Schema (Zod)

// Auto-extracted from Props interface — see source for authoritative types.
// Component: FeatureCards
// 2 props detected · 0 port(s)

Feature Cards

feature_cards

Feature Cards — Andy's hand-crafted Remotion scene. 2 props. no ports detected — source-only preview.

Workflow Inputs (0)

This block has no workflow-connected inputs.

Config Fields (2)

  • cards
  • backgroundColor

Meta

Updated
4/21/2026