Catalog

Edit Props (live)

config
config
config
config
config
config
config
config

Source

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

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

type FaviconType =
  | "wireflow"
  | "linkedin"
  | "instagram"
  | "youtube"
  | "mockflow"
  | "uxpilot"
  | "producthunt"
  | "generic";

interface ResultItem {
  title: string;
  url?: string;
  snippet?: string;
  faviconText?: string;
  favicon?: FaviconType;
  siteName?: string; // shown above URL, e.g. "LinkedIn · Wireflow AI"
  meta?: string; // shown under site name, e.g. "590+ followers"
}

interface SearchBarProps {
  query: string;
  engine?: "google" | "youtube" | "chatgpt" | "generic";
  results?: ResultItem[];
  backgroundColor?: string;
  typingCps?: number;
  clickFirstResult?: boolean;
  autocomplete?: string[];
  // Show the full Google SERP (results page) after the query is submitted
  showResultsPage?: boolean;
}

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

// ─── Favicon component ────────────────────────────────────────
const FaviconIcon: React.FC<{ type?: FaviconType; fallbackText?: string; size?: number }> = ({
  type,
  fallbackText,
  size = 52,
}) => {
  const s = size;
  if (type === "wireflow") {
    return (
      <div
        style={{
          width: s,
          height: s,
          borderRadius: s * 0.27,
          overflow: "hidden",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        <Img
          src={staticFile("logos/wireflow.png")}
          style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
        />
      </div>
    );
  }
  if (type === "linkedin") {
    return (
      <svg width={s} height={s} viewBox="0 0 52 52">
        <rect width="52" height="52" rx="10" fill="#0a66c2" />
        <text
          x="26"
          y="36"
          fontFamily="Inter, sans-serif"
          fontSize="26"
          fontWeight="900"
          fill="#fff"
          textAnchor="middle"
        >
          in
        </text>
      </svg>
    );
  }
  if (type === "instagram") {
    return (
      <svg width={s} height={s} viewBox="0 0 52 52">
        <defs>
          <linearGradient id="ig" x1="0" y1="1" x2="1" y2="0">
            <stop offset="0%" stopColor="#f9ce34" />
            <stop offset="50%" stopColor="#ee2a7b" />
            <stop offset="100%" stopColor="#6228d7" />
          </linearGradient>
        </defs>
        <rect width="52" height="52" rx="14" fill="url(#ig)" />
        <rect x="12" y="12" width="28" height="28" rx="8" fill="none" stroke="#fff" strokeWidth="2.6" />
        <circle cx="26" cy="26" r="7" fill="none" stroke="#fff" strokeWidth="2.6" />
        <circle cx="34" cy="18" r="2" fill="#fff" />
      </svg>
    );
  }
  if (type === "youtube") {
    return (
      <svg width={s} height={s} viewBox="0 0 52 52">
        <rect width="52" height="52" rx="14" fill="#fff" />
        <rect x="6" y="14" width="40" height="24" rx="6" fill="#ff0000" />
        <path d="M22 20 L34 26 L22 32 Z" fill="#fff" />
      </svg>
    );
  }
  if (type === "mockflow") {
    return (
      <svg width={s} height={s} viewBox="0 0 52 52">
        <rect width="52" height="52" rx="14" fill="#fff" />
        <path d="M10 28 L42 14 L34 28 L42 38 Z" fill="#29b6f6" />
      </svg>
    );
  }
  if (type === "uxpilot") {
    return (
      <svg width={s} height={s} viewBox="0 0 52 52">
        <rect width="52" height="52" rx="26" fill="#7c3aed" />
        <circle cx="19" cy="23" r="3" fill="#fff" />
        <circle cx="33" cy="23" r="3" fill="#fff" />
        <path d="M18 33 Q26 38, 34 33" stroke="#fff" strokeWidth="2.6" fill="none" strokeLinecap="round" />
      </svg>
    );
  }
  if (type === "producthunt") {
    return (
      <svg width={s} height={s} viewBox="0 0 52 52">
        <circle cx="26" cy="26" r="26" fill="#da552f" />
        <text x="26" y="34" fontFamily="Inter, sans-serif" fontSize="22" fontWeight="900" fill="#fff" textAnchor="middle">
          P
        </text>
      </svg>
    );
  }
  // generic — dark circle with letter
  return (
    <svg width={s} height={s} viewBox="0 0 52 52">
      <circle cx="26" cy="26" r="26" fill="#1a1a2e" />
      <text
        x="26"
        y="34"
        fontFamily="Inter, sans-serif"
        fontSize="22"
        fontWeight="700"
        fill="#fff"
        textAnchor="middle"
      >
        {fallbackText || "?"}
      </text>
    </svg>
  );
};

// ─── Multi-color Google G (for compact SERP bar) ───────────
const GoogleGLogo: React.FC<{ size?: number }> = ({ size = 36 }) => (
  <svg width={size} height={size} viewBox="0 0 48 48">
    <path fill="#4285F4" d="M43.6 20.5H42V20H24v8h11.3c-1.7 4.7-6.2 8-11.3 8-6.6 0-12-5.4-12-12s5.4-12 12-12c3.1 0 5.8 1.1 7.9 3l5.7-5.7C33.6 6.2 29 4 24 4 13 4 4 13 4 24s9 20 20 20 20-9 20-20c0-1.3-.1-2.4-.4-3.5z" />
    <path fill="#EA4335" d="M6.3 14.1l6.6 4.8C14.6 15 18.9 12 24 12c3.1 0 5.8 1.1 7.9 3l5.7-5.7C33.6 6.2 29 4 24 4 16.3 4 9.7 8.3 6.3 14.1z" />
    <path fill="#FBBC05" d="M24 44c5 0 9.5-1.9 12.9-5.1l-6-5.1c-1.7 1.2-4 2.2-6.9 2.2-5 0-9.5-3.3-11.2-7.9l-6.6 5.1C9.4 39.5 16.1 44 24 44z" />
    <path fill="#34A853" d="M43.6 20.5H42V20H24v8h11.3c-.8 2.2-2.1 4-3.8 5.3l6 5.1c-.4.4 6.5-4.7 6.5-14.4 0-1.3-.1-2.4-.4-3.5z" />
  </svg>
);

export const SearchBar: React.FC<SearchBarProps> = ({
  query,
  engine = "google",
  results = [],
  backgroundColor = "#ffffff",
  typingCps = 10,
  clickFirstResult = false,
  autocomplete,
  showResultsPage = false,
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const t = frame / fps;

  // Timing
  const typingStart = 0.75;
  const typeDur = query.length / typingCps;
  const typingEnd = typingStart + typeDur;
  const enterAt = typingEnd + 0.2;
  // Transition to SERP begins shortly after enter
  const serpStart = showResultsPage ? enterAt + 0.35 : typingEnd + 0.35;
  const serpSettle = serpStart + 0.5;
  // When the results list is "settled", move cursor to first result
  const resultsListStart = showResultsPage ? serpSettle : typingEnd + 0.35;
  const clickResultStart = resultsListStart + 0.9;
  const clickResultEnd = clickResultStart + 0.45;

  const W = 1080;
  const H = 1920;

  // Homepage layout
  const barY = 700;
  const barHeight = 100;
  const barWidth = 920;
  const barX = (W - barWidth) / 2;

  // SERP layout
  const serpBarY = 110;
  const serpBarHeight = 80;
  const serpBarWidth = W - 160;
  const serpBarX = 80;

  // Homepage fade (out if showResultsPage)
  const homepageOp = showResultsPage
    ? interpolate(t, [0, 0.3, serpStart, serpSettle], [0, 1, 1, 0], {
        extrapolateRight: "clamp",
      })
    : interpolate(t, [0, 0.3], [0, 1], { extrapolateRight: "clamp" });

  // SERP fade in
  const serpOp = showResultsPage
    ? interpolate(t, [serpStart, serpSettle], [0, 1], {
        extrapolateLeft: "clamp",
        extrapolateRight: "clamp",
      })
    : 0;

  // SERP slide in (results list rises from below)
  const serpSlideY = showResultsPage
    ? interpolate(t, [serpStart, serpSettle], [40, 0], {
        extrapolateLeft: "clamp",
        extrapolateRight: "clamp",
        easing: Easing.out(Easing.cubic),
      })
    : 0;

  // ─── Cursor path ──────────────────────────────────────
  // Phase 1: top-right → homepage search bar center
  const cursorStartX = W - 180;
  const cursorStartY = 240;
  const homepageBarCursorX = W / 2;
  const homepageBarCursorY = barY + barHeight / 2;

  const cursorToBarT = interpolate(t, [0.25, 0.75], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });
  const phase1X = interpolate(cursorToBarT, [0, 1], [cursorStartX, homepageBarCursorX]);
  const phase1Y = interpolate(cursorToBarT, [0, 1], [cursorStartY, homepageBarCursorY]);

  // Phase 2: during enter/transition, hide cursor OR fly it to near the results area.
  // We hide cursor during the SERP transition to keep things clean, then re-show it.

  // Phase 3: on SERP, cursor appears at offscreen top-right again then glides to first result
  const firstResultX = 140;
  const firstResultY = 340;
  const phase3From: [number, number] = [W - 200, 220];
  const cursorToResultT = clickFirstResult && showResultsPage
    ? interpolate(t, [resultsListStart + 0.3, clickResultStart], [0, 1], {
        extrapolateLeft: "clamp",
        extrapolateRight: "clamp",
        easing: Easing.inOut(Easing.cubic),
      })
    : 0;
  const phase3X = interpolate(cursorToResultT, [0, 1], [phase3From[0], firstResultX]);
  const phase3Y = interpolate(cursorToResultT, [0, 1], [phase3From[1], firstResultY]);

  // Blend cursor position based on phase
  let cursorX = phase1X;
  let cursorY = phase1Y;
  if (showResultsPage && t > serpStart) {
    cursorX = phase3X;
    cursorY = phase3Y;
  }

  // Cursor visibility — hidden during SERP transition, reappears once settled
  const cursorOp = showResultsPage
    ? interpolate(
        t,
        [0.1, 0.3, enterAt + 0.1, serpSettle - 0.05, serpSettle + 0.15, clickResultEnd + 0.4, clickResultEnd + 0.7],
        [0, 1, 1, 0, 1, 1, 0],
        { extrapolateRight: "clamp" }
      )
    : interpolate(t, [0.1, 0.3, clickResultEnd + 0.2, clickResultEnd + 0.5], [0, 1, 1, 0], {
        extrapolateRight: "clamp",
      });

  // Homepage bar focus + enter pulse
  const focusScale = interpolate(t, [0.75, 0.82, 0.9], [1, 0.975, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const enterPulse = interpolate(t, [typingEnd, typingEnd + 0.18], [1, 1.02], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  // Query text — char by char
  const typeT = interpolate(t, [typingStart, typingEnd], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const shownChars = Math.floor(typeT * query.length);
  const shownQuery = query.slice(0, shownChars);
  const cursorBlink = Math.floor(t * 2) % 2 === 0;
  const isFocused = t > 0.65 && t < enterAt;

  // Autocomplete dropdown
  const suggestions =
    autocomplete ||
    (engine === "google" && query
      ? [
          query,
          `${query.split(" ")[0]} generator`,
          `${query.split(" ")[0]} vs user flow`,
          query.replace(/\s+/g, "").slice(0, 7),
          query.split(" ")[0].slice(0, 4) + " aid",
          `aero${query.split(" ")[0]} wiring harness`,
          `air${query.split(" ")[0].slice(-3)} australia`,
          `https://${query.split(" ")[0]}.co/`,
        ]
      : []);
  const dropdownOp = interpolate(
    t,
    [typingStart - 0.1, typingStart + 0.1, enterAt - 0.1, enterAt + 0.05],
    [0, 1, 1, 0],
    { extrapolateRight: "clamp" }
  );

  const LOGO_COLORS = ["#4285F4", "#EA4335", "#FBBC05", "#4285F4", "#34A853", "#EA4335"];
  const logoChars = "Google".split("");
  const visibleResults = results;

  return (
    <AbsoluteFill style={{ backgroundColor, fontFamily: FONT, overflow: "hidden" }}>
      {/* ═══ HOMEPAGE VIEW ═══ */}
      <AbsoluteFill style={{ opacity: homepageOp, pointerEvents: "none" }}>
        {/* Top-right nav */}
        <div
          style={{
            position: "absolute",
            top: 40,
            right: 40,
            display: "flex",
            alignItems: "center",
            gap: 32,
            fontSize: 26,
            color: "#1f1f1f",
          }}
        >
          <span>Gmail</span>
          <span>Images</span>
          <div
            style={{
              display: "grid",
              gridTemplateColumns: "repeat(3, 8px)",
              gridTemplateRows: "repeat(3, 8px)",
              gap: 4,
              padding: 12,
              borderRadius: "50%",
            }}
          >
            {Array.from({ length: 9 }).map((_, i) => (
              <div key={i} style={{ width: 8, height: 8, borderRadius: "50%", background: "#5f6368" }} />
            ))}
          </div>
          <div
            style={{
              width: 56,
              height: 56,
              borderRadius: "50%",
              background: "#a142f4",
              color: "#fff",
              fontWeight: 700,
              fontSize: 28,
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
            }}
          >
            A
          </div>
        </div>
        <div style={{ position: "absolute", top: 48, left: 40, display: "flex", gap: 30, fontSize: 26 }}>
          <span>About</span>
          <span>Store</span>
        </div>

        {/* Google logo */}
        <div
          style={{
            position: "absolute",
            top: 410,
            left: 0,
            right: 0,
            textAlign: "center",
            fontSize: 164,
            fontWeight: 500,
            letterSpacing: -6,
          }}
        >
          {logoChars.map((c, i) => (
            <span key={i} style={{ color: LOGO_COLORS[i % LOGO_COLORS.length] }}>
              {c}
            </span>
          ))}
        </div>

        {/* Homepage search bar */}
        <div
          style={{
            position: "absolute",
            left: barX,
            top: barY,
            width: barWidth,
            transform: `scale(${focusScale * enterPulse})`,
            transformOrigin: "center top",
          }}
        >
          <div
            style={{
              width: "100%",
              height: barHeight,
              borderRadius: dropdownOp > 0.5 ? "28px 28px 0 0" : 28,
              border: "1px solid #dfe1e5",
              background: "#fff",
              display: "flex",
              alignItems: "center",
              padding: "0 22px",
              gap: 18,
              fontSize: 38,
              color: "#1f1f1f",
              boxShadow: "0 1px 6px rgba(32,33,36,0.12)",
            }}
          >
            <svg width="32" height="32" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
              <path d="M12 5v14M5 12h14" stroke="#5f6368" strokeWidth="2.4" strokeLinecap="round" />
            </svg>
            <div style={{ flex: 1, display: "flex", alignItems: "center", minWidth: 0 }}>
              {shownQuery ? <span>{shownQuery}</span> : null}
              {isFocused && cursorBlink && (
                <span
                  style={{
                    display: "inline-block",
                    width: 2,
                    height: 44,
                    background: "#4285F4",
                    marginLeft: 2,
                  }}
                />
              )}
            </div>
            <svg width="28" height="28" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
              <path d="M6 6l12 12M18 6L6 18" stroke="#5f6368" strokeWidth="2.2" strokeLinecap="round" />
            </svg>
            <div style={{ width: 1, height: 40, background: "#dadce0" }} />
            <div
              style={{
                display: "flex",
                alignItems: "center",
                gap: 8,
                padding: "10px 18px",
                borderRadius: 22,
                background: "#e8f0fe",
                color: "#1967d2",
                fontSize: 24,
                fontWeight: 500,
                flexShrink: 0,
              }}
            >
              <svg width="22" height="22" viewBox="0 0 24 24" fill="none">
                <path d="M12 2l2 6 6 2-6 2-2 6-2-6-6-2 6-2z" fill="#1967d2" />
              </svg>
              AI Mode
            </div>
          </div>

          {/* Autocomplete dropdown */}
          {suggestions.length > 0 && (
            <div
              style={{
                opacity: dropdownOp,
                background: "#fff",
                border: "1px solid #dfe1e5",
                borderTop: "none",
                borderRadius: "0 0 28px 28px",
                boxShadow: "0 4px 6px rgba(32,33,36,0.12)",
                overflow: "hidden",
                marginTop: -1,
              }}
            >
              <div style={{ height: 1, background: "#e8eaed", margin: "0 22px" }} />
              {suggestions.slice(0, 8).map((s, i) => {
                const lower = s.toLowerCase();
                const typedLower = shownQuery.toLowerCase();
                const matchIdx = lower.indexOf(typedLower);
                let before = s, matched = "", after = "";
                if (typedLower && matchIdx === 0) {
                  matched = s.slice(0, typedLower.length);
                  after = s.slice(typedLower.length);
                  before = "";
                }
                return (
                  <div
                    key={i}
                    style={{
                      display: "flex",
                      alignItems: "center",
                      gap: 18,
                      padding: "12px 22px",
                      fontSize: 28,
                      color: "#1f1f1f",
                    }}
                  >
                    <svg width="22" height="22" viewBox="0 0 24 24" fill="none">
                      <circle cx="10.5" cy="10.5" r="6" stroke="#9aa0a6" strokeWidth="2" />
                      <path d="m15 15 4.5 4.5" stroke="#9aa0a6" strokeWidth="2" strokeLinecap="round" />
                    </svg>
                    <div>
                      <span>{matched}</span>
                      <span style={{ fontWeight: 700 }}>{after}</span>
                      {!matched && <span>{before}</span>}
                    </div>
                  </div>
                );
              })}
              <div
                style={{
                  display: "flex",
                  justifyContent: "center",
                  gap: 14,
                  padding: "18px 0 24px",
                }}
              >
                {["Google Search", "I'm Feeling Lucky"].map((label) => (
                  <div
                    key={label}
                    style={{
                      padding: "12px 22px",
                      background: "#f8f9fa",
                      border: "1px solid #f8f9fa",
                      borderRadius: 6,
                      fontSize: 22,
                      color: "#3c4043",
                    }}
                  >
                    {label}
                  </div>
                ))}
              </div>
              <div
                style={{
                  textAlign: "right",
                  padding: "0 22px 16px",
                  fontSize: 18,
                  color: "#70757a",
                }}
              >
                Report inappropriate predictions
              </div>
            </div>
          )}
        </div>
      </AbsoluteFill>

      {/* ═══ SERP (Results Page) VIEW ═══ */}
      {showResultsPage && (
        <AbsoluteFill style={{ opacity: serpOp, pointerEvents: "none" }}>
          {/* Top compact search bar with G logo + query + icons */}
          <div
            style={{
              position: "absolute",
              top: serpBarY,
              left: serpBarX,
              width: serpBarWidth,
              height: serpBarHeight,
              borderRadius: 40,
              border: "1px solid #dfe1e5",
              background: "#fff",
              display: "flex",
              alignItems: "center",
              padding: "0 20px",
              gap: 16,
              boxShadow: "0 1px 4px rgba(32,33,36,0.08)",
            }}
          >
            <GoogleGLogo size={42} />
            <div style={{ flex: 1, fontSize: 32, color: "#1f1f1f", minWidth: 0 }}>{query}</div>
            {/* X */}
            <svg width="26" height="26" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
              <path d="M6 6l12 12M18 6L6 18" stroke="#5f6368" strokeWidth="2.2" strokeLinecap="round" />
            </svg>
            <div style={{ width: 1, height: 32, background: "#dadce0" }} />
            {/* Mic */}
            <svg width="28" height="28" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
              <rect x="9" y="3" width="6" height="12" rx="3" fill="#4285F4" />
              <path d="M6 11v1a6 6 0 0 0 12 0v-1M12 18v3" stroke="#4285F4" strokeWidth="2" strokeLinecap="round" />
            </svg>
            {/* Camera (lens) */}
            <svg width="28" height="28" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
              <rect x="3" y="7" width="18" height="13" rx="3" stroke="#EA4335" strokeWidth="2" />
              <circle cx="12" cy="13" r="3.5" stroke="#EA4335" strokeWidth="2" />
              <rect x="8" y="4" width="8" height="3" rx="1" fill="#EA4335" />
            </svg>
            {/* Search magnifier */}
            <svg width="28" height="28" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
              <circle cx="10.5" cy="10.5" r="6" stroke="#4285F4" strokeWidth="2.2" />
              <path d="m15 15 4.5 4.5" stroke="#4285F4" strokeWidth="2.2" strokeLinecap="round" />
            </svg>
          </div>

          {/* Top-right nav mini (apps grid + avatar) */}
          <div
            style={{
              position: "absolute",
              top: serpBarY + 10,
              right: 40,
              display: "flex",
              alignItems: "center",
              gap: 14,
            }}
          >
            <div
              style={{
                display: "grid",
                gridTemplateColumns: "repeat(3, 7px)",
                gap: 4,
                padding: 10,
              }}
            >
              {Array.from({ length: 9 }).map((_, i) => (
                <div key={i} style={{ width: 7, height: 7, borderRadius: "50%", background: "#5f6368" }} />
              ))}
            </div>
            <div
              style={{
                width: 52,
                height: 52,
                borderRadius: "50%",
                background: "#a142f4",
                color: "#fff",
                fontWeight: 700,
                fontSize: 26,
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
              }}
            >
              A
            </div>
          </div>

          {/* Results list */}
          <div
            style={{
              position: "absolute",
              top: serpBarY + serpBarHeight + 50,
              left: 120,
              right: 120,
              transform: `translateY(${serpSlideY}px)`,
            }}
          >
            {visibleResults.map((r, i) => {
              const startAt = resultsListStart + 0.08 + i * 0.1;
              const op = interpolate(t, [startAt, startAt + 0.35], [0, 1], {
                extrapolateLeft: "clamp",
                extrapolateRight: "clamp",
              });
              const yOff = interpolate(t, [startAt, startAt + 0.35], [20, 0], {
                extrapolateLeft: "clamp",
                extrapolateRight: "clamp",
                easing: Easing.out(Easing.cubic),
              });
              const isFirst = i === 0;
              const hoverOp =
                clickFirstResult && isFirst
                  ? interpolate(t, [clickResultStart - 0.1, clickResultStart + 0.05], [0, 1], {
                      extrapolateLeft: "clamp",
                      extrapolateRight: "clamp",
                    })
                  : 0;
              const clickScale =
                clickFirstResult && isFirst
                  ? interpolate(
                      t,
                      [clickResultEnd, clickResultEnd + 0.08, clickResultEnd + 0.2],
                      [1, 0.97, 1],
                      { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
                    )
                  : 1;

              const domain = r.url ? r.url.replace(/^https?:\/\//, "").split("/")[0] : "";
              const urlDisplay = r.url
                ? r.url.replace(/^https?:\/\//, "https://")
                : "";

              return (
                <div
                  key={i}
                  style={{
                    marginBottom: 38,
                    opacity: op,
                    transform: `translateY(${yOff}px) scale(${clickScale})`,
                    padding: "14px 16px",
                    marginLeft: -16,
                    marginRight: -16,
                    borderRadius: 12,
                    background: `rgba(66,133,244,${hoverOp * 0.07})`,
                    boxShadow: hoverOp > 0.5 ? "0 0 0 2px rgba(66,133,244,0.3)" : "none",
                  }}
                >
                  {/* Site header row */}
                  <div style={{ display: "flex", alignItems: "center", gap: 16, marginBottom: 12 }}>
                    <FaviconIcon type={r.favicon} fallbackText={r.faviconText} size={52} />
                    <div style={{ display: "flex", flexDirection: "column", flex: 1, minWidth: 0 }}>
                      <div
                        style={{
                          fontSize: 24,
                          color: "#1f1f1f",
                          fontWeight: 500,
                          lineHeight: 1.15,
                          marginBottom: 4,
                        }}
                      >
                        {r.siteName || domain.replace(/^www\./, "")}
                      </div>
                      <div style={{ fontSize: 20, color: "#4d5156", lineHeight: 1.15 }}>
                        {r.meta || urlDisplay}
                      </div>
                    </div>
                    <div style={{ color: "#5f6368", fontSize: 28, letterSpacing: 2, marginRight: 4 }}>⋮</div>
                  </div>
                  {/* Title */}
                  <div
                    style={{
                      fontSize: 38,
                      fontWeight: 500,
                      color: isFirst && hoverOp > 0.2 ? "#1967d2" : "#7b1fa2",
                      marginBottom: 10,
                      lineHeight: 1.2,
                      textDecoration: isFirst && hoverOp > 0.4 ? "underline" : "none",
                    }}
                  >
                    {r.title}
                  </div>
                  {/* Snippet */}
                  {r.snippet && (
                    <div style={{ fontSize: 22, color: "#4d5156", lineHeight: 1.4 }}>{r.snippet}</div>
                  )}
                </div>
              );
            })}
          </div>

          {/* Click ripple on first result (SERP mode) */}
          {clickFirstResult && t > clickResultEnd - 0.05 && t < clickResultEnd + 0.55 && (
            <div
              style={{
                position: "absolute",
                left: firstResultX,
                top: firstResultY,
                width: interpolate(t, [clickResultEnd, clickResultEnd + 0.45], [0, 240], {
                  extrapolateLeft: "clamp",
                  extrapolateRight: "clamp",
                }),
                height: interpolate(t, [clickResultEnd, clickResultEnd + 0.45], [0, 240], {
                  extrapolateLeft: "clamp",
                  extrapolateRight: "clamp",
                }),
                transform: "translate(-50%, -50%)",
                borderRadius: "50%",
                border: "3px solid #4285F4",
                opacity: interpolate(t, [clickResultEnd, clickResultEnd + 0.45], [0.75, 0], {
                  extrapolateLeft: "clamp",
                  extrapolateRight: "clamp",
                }),
                pointerEvents: "none",
                zIndex: 50,
              }}
            />
          )}
        </AbsoluteFill>
      )}

      {/* Animated cursor (over both phases) */}
      <div
        style={{
          position: "absolute",
          left: cursorX,
          top: cursorY,
          width: 48,
          height: 48,
          pointerEvents: "none",
          opacity: cursorOp,
          zIndex: 100,
          filter: "drop-shadow(0 2px 6px rgba(0,0,0,0.4))",
        }}
      >
        <svg viewBox="0 0 24 24" width="48" height="48">
          <path
            d="M4,2 L4,18 L8,14 L11,20 L14,19 L11,13 L17,13 Z"
            fill="#fff"
            stroke="#000"
            strokeWidth="1.5"
            strokeLinejoin="round"
          />
        </svg>
      </div>
    </AbsoluteFill>
  );
};

Schema (Zod)

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

Search Bar

search_bar

Search Bar — Andy's hand-crafted Remotion scene. 8 props. no ports detected — source-only preview.

Workflow Inputs (0)

This block has no workflow-connected inputs.

Config Fields (8)

  • query
  • engine
  • results
  • typingCps
  • autocomplete
  • backgroundColor
  • showResultsPage
  • clickFirstResult

Meta

Updated
4/21/2026