shadimage

Hero Device Assemble

Floating device layers spring together into a laptop or phone mockup, then the screen wakes with a shimmer.

registry/bases/editframe/components/hero-device-assemble/index.tsx

"use client";

import { Timegroup } from "@editframe/react";
import type { CSSProperties } from "react";

const FONT_FAMILY =
  "var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif";

const MockUI = ({ accentColor }: { accentColor: string }) => (
  <div
    style={{
      background: "#0b0b0f",
      color: "white",
      display: "flex",
      flexDirection: "column",
      fontFamily: FONT_FAMILY,
      inset: 0,
      overflow: "hidden",
      position: "absolute",
    }}
  >
    {/* Title bar */}
    <div
      style={{
        alignItems: "center",
        background: "#111118",
        borderBottom: "1px solid rgba(255,255,255,0.06)",
        display: "flex",
        gap: 8,
        height: "10%",
        padding: "0 16px",
      }}
    >
      <div
        style={{
          background: "rgba(255,255,255,0.18)",
          borderRadius: "50%",
          height: 10,
          width: 10,
        }}
      />
      <div
        style={{
          background: "rgba(255,255,255,0.18)",
          borderRadius: "50%",
          height: 10,
          width: 10,
        }}
      />
      <div
        style={{
          background: "rgba(255,255,255,0.18)",
          borderRadius: "50%",
          height: 10,
          width: 10,
        }}
      />
    </div>
    <div style={{ display: "flex", flex: 1 }}>
      {/* Sidebar */}
      <div
        style={{
          background: "#0e0e14",
          borderRight: "1px solid rgba(255,255,255,0.05)",
          display: "flex",
          flexDirection: "column",
          gap: 10,
          padding: 16,
          width: "22%",
        }}
      >
        <div
          style={{
            background: accentColor,
            borderRadius: 4,
            height: 12,
            opacity: 0.85,
            width: "70%",
          }}
        />
        <div
          style={{
            background: "rgba(255,255,255,0.1)",
            borderRadius: 4,
            height: 10,
            width: "85%",
          }}
        />
        <div
          style={{
            background: "rgba(255,255,255,0.08)",
            borderRadius: 4,
            height: 10,
            width: "60%",
          }}
        />
        <div
          style={{
            background: "rgba(255,255,255,0.08)",
            borderRadius: 4,
            height: 10,
            width: "75%",
          }}
        />
      </div>
      {/* Content */}
      <div
        style={{
          display: "flex",
          flex: 1,
          flexDirection: "column",
          gap: 16,
          padding: 24,
        }}
      >
        <div
          style={{
            background: "rgba(255,255,255,0.85)",
            borderRadius: 4,
            height: 18,
            width: "40%",
          }}
        />
        <div
          style={{
            background: "rgba(255,255,255,0.18)",
            borderRadius: 4,
            height: 10,
            width: "65%",
          }}
        />
        <div
          style={{
            display: "grid",
            gap: 12,
            gridTemplateColumns: "1fr 1fr 1fr",
            marginTop: 12,
          }}
        >
          <div
            style={{
              background:
                "linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02))",
              border: "1px solid rgba(255,255,255,0.06)",
              borderRadius: 8,
              height: 90,
            }}
          />
          <div
            style={{
              background: `linear-gradient(180deg, ${accentColor}33, ${accentColor}11)`,
              border: `1px solid ${accentColor}55`,
              borderRadius: 8,
              height: 90,
            }}
          />
          <div
            style={{
              background:
                "linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02))",
              border: "1px solid rgba(255,255,255,0.06)",
              borderRadius: 8,
              height: 90,
            }}
          />
        </div>
        <div
          style={{
            background:
              "linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.01))",
            border: "1px solid rgba(255,255,255,0.06)",
            borderRadius: 8,
            flex: 1,
          }}
        />
      </div>
    </div>
  </div>
);

export interface HeroDeviceAssembleProps {
  assembleStart?: number;
  device?: "laptop" | "phone";
  accentColor?: string;
  speed?: number;
  fps?: number;
  durationInFrames?: number;
  width?: number;
  height?: number;
  className?: string;
}

export const HeroDeviceAssemble = ({
  assembleStart = 0,
  device = "laptop",
  accentColor = "#22c55e",
  speed = 1,
  fps = 30,
  durationInFrames = 150,
  width = 1280,
  height = 720,
  className,
}: HeroDeviceAssembleProps) => {
  const safeSpeed = Math.max(0.01, speed);
  const durationMs = (durationInFrames / fps) * 1000;
  const frameMs = 1000 / fps;
  const assembleDelayMs = assembleStart * frameMs;
  const assembleMs = ((60 / fps) * 1000) / safeSpeed;
  const settleMs = ((45 / fps) * 1000) / safeSpeed;
  const shimmerMs = ((30 / fps) * 1000) / safeSpeed;

  const isPhone = device === "phone";
  const deviceW = isPhone ? 320 : 760;
  const deviceH = isPhone ? 640 : 470;
  const screenInset = isPhone ? 12 : 18;
  const bezelRadius = isPhone ? 36 : 14;

  const containerStyle: CSSProperties = {
    alignItems: "center",
    background: "radial-gradient(ellipse at center, #1a1a22 0%, #050507 70%)",
    display: "flex",
    fontFamily: FONT_FAMILY,
    height,
    justifyContent: "center",
    overflow: "hidden",
    perspective: 2000,
    position: "relative",
    width,
  };

  return (
    <Timegroup
      className={className}
      duration={`${durationMs}ms`}
      mode="fixed"
      style={containerStyle}
    >
      <>
        <style>{`
          @keyframes framecn-hero-assemble-lid {
            from { transform: translateZ(1000px); }
            to { transform: translateZ(0); }
          }
          @keyframes framecn-hero-assemble-base {
            from { transform: translateZ(-800px) rotateX(78deg); }
            to { transform: translateZ(0) rotateX(0deg); }
          }
          @keyframes framecn-hero-assemble-bezel {
            from { transform: translateZ(600px); }
            to { transform: translateZ(0); }
          }
          @keyframes framecn-hero-assemble-screen {
            from { transform: translateZ(300px); opacity: 0; }
            to { transform: translateZ(0); opacity: 1; }
          }
          @keyframes framecn-hero-shimmer {
            from { transform: translateX(-100%); }
            to { transform: translateX(200%); }
          }
        `}</style>
        <div
          style={{
            animation: `framecn-hero-assemble-lid ${assembleMs}ms cubic-bezier(0.16, 1, 0.3, 1) ${assembleDelayMs}ms backwards`,
            height: deviceH,
            position: "relative",
            transformStyle: "preserve-3d",
            width: deviceW,
          }}
        >
          {/* Back lid */}
          <div
            style={{
              animation: `framecn-hero-assemble-lid ${assembleMs}ms cubic-bezier(0.16, 1, 0.3, 1) ${assembleDelayMs}ms backwards`,
              background: "linear-gradient(180deg, #1f2128 0%, #0e1014 100%)",
              border: "1px solid rgba(255,255,255,0.08)",
              borderRadius: bezelRadius + 4,
              boxShadow:
                "0 60px 120px rgba(0,0,0,0.7), inset 0 1px 0 rgba(255,255,255,0.06)",
              inset: -4,
              position: "absolute",
            }}
          />

          {/* Keyboard / chassis base - laptop only */}
          {!isPhone && (
            <div
              style={{
                animation: `framecn-hero-assemble-base ${assembleMs}ms cubic-bezier(0.16, 1, 0.3, 1) ${assembleDelayMs}ms backwards`,
                background: "linear-gradient(180deg, #2a2d36 0%, #14161c 100%)",
                border: "1px solid rgba(255,255,255,0.06)",
                borderRadius: "0 0 12px 12px",
                bottom: -28,
                boxShadow: "0 30px 60px rgba(0,0,0,0.6)",
                height: 28,
                left: -40,
                position: "absolute",
                right: -40,
              }}
            />
          )}

          {/* Bezel frame */}
          <div
            style={{
              animation: `framecn-hero-assemble-bezel ${assembleMs}ms cubic-bezier(0.16, 1, 0.3, 1) ${assembleDelayMs}ms backwards`,
              background: "#0a0a0d",
              border: "1px solid rgba(255,255,255,0.12)",
              borderRadius: bezelRadius,
              boxShadow: "inset 0 0 0 2px rgba(255,255,255,0.04)",
              inset: 0,
              position: "absolute",
            }}
          />

          {/* UI screen */}
          <div
            style={{
              animation: `framecn-hero-assemble-screen ${assembleMs * 0.8}ms cubic-bezier(0.16, 1, 0.3, 1) ${assembleDelayMs + assembleMs * 0.2}ms backwards`,
              background: "black",
              borderRadius: bezelRadius - 6,
              inset: screenInset,
              overflow: "hidden",
              position: "absolute",
            }}
          >
            {/* Black panel during flight */}
            <div
              style={{
                animation: `framecn-hero-assemble-screen ${settleMs}ms ease-out ${assembleDelayMs + assembleMs}ms forwards`,
                inset: 0,
                position: "absolute",
              }}
            />
            {/* UI fades in */}
            <div
              style={{
                animation: `framecn-hero-assemble-screen ${shimmerMs}ms ease-out ${assembleDelayMs + assembleMs + settleMs}ms backwards`,
                inset: 0,
                position: "absolute",
              }}
            >
              <MockUI accentColor={accentColor} />
            </div>
            {/* Shimmer sweep */}
            <div
              style={{
                animation: `framecn-hero-shimmer ${shimmerMs}ms ease-in-out ${assembleDelayMs + assembleMs + settleMs + 100}ms backwards`,
                background:
                  "linear-gradient(115deg, transparent 40%, rgba(255,255,255,0.18) 50%, transparent 60%)",
                inset: 0,
                mixBlendMode: "screen",
                pointerEvents: "none",
                position: "absolute",
              }}
            />
          </div>
        </div>
      </>
    </Timegroup>
  );
};
Where it goes
hero

DependenciesBundle weight · light

remotion