{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "browser-flow", "title": "Browser Flow", "description": "Full Safari/Chrome simulation: typed URL, progress bar, page render, scroll, and a virtual cursor click.", "dependencies": [ "remotion" ], "files": [ { "path": "registry/remocn/browser-flow/index.tsx", "content": "\"use client\";\n\nimport { interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport interface BrowserFlowProps {\n url?: string;\n speed?: number;\n className?: string;\n}\n\nconst FONT_FAMILY =\n \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\";\nconst MONO_FAMILY =\n \"var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, monospace\";\n\nfunction MacDots({ size = 12 }: { size?: number }) {\n return (\n
\n {[\"#ff5f57\", \"#febc2e\", \"#28c840\"].map((c) => (\n \n ))}\n
\n );\n}\n\nfunction CursorSVG() {\n return (\n \n \n \n );\n}\n\nfunction easeInOut(t: number) {\n return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;\n}\n\nexport function BrowserFlow({\n url = \"remocn.dev\",\n speed = 1,\n className,\n}: BrowserFlowProps) {\n const frame = useCurrentFrame() * speed;\n const { fps } = useVideoConfig();\n\n // ── Browser dimensions ────────────────────────────\n const browserWidth = 1100;\n const browserHeight = 620;\n const chromeHeight = 56;\n const browserLeft = (1280 - browserWidth) / 2;\n const browserTop = (720 - browserHeight) / 2;\n\n // ── URL typing (frames 0..40) ─────────────────────\n const typingProgress = interpolate(frame, [4, 38], [0, url.length], {\n extrapolateLeft: \"clamp\",\n extrapolateRight: \"clamp\",\n });\n const typedChars = Math.floor(typingProgress);\n const typedUrl = url.slice(0, typedChars);\n const showCaret = frame < 50 && Math.floor(frame / 12) % 2 === 0;\n\n // Autocomplete suggestion appears once a few chars typed\n const showSuggestion = typedChars >= 2 && frame < 48;\n const suggestion = url;\n\n // ── Progress bar ──────────────────────────────────\n // 40..50: 0 -> 15\n // 50..65: hold at 15\n // 65..85: 15 -> 100\n // 85..95: fade out\n let progress = 0;\n if (frame < 40) progress = 0;\n else if (frame < 50)\n progress = interpolate(frame, [40, 50], [0, 15], {\n extrapolateRight: \"clamp\",\n });\n else if (frame < 65) progress = 15;\n else if (frame < 85)\n progress = interpolate(frame, [65, 85], [15, 100], {\n extrapolateRight: \"clamp\",\n });\n else progress = 100;\n\n const progressOpacity =\n frame < 40\n ? 0\n : frame < 85\n ? 1\n : interpolate(frame, [85, 95], [1, 0], {\n extrapolateRight: \"clamp\",\n });\n\n // ── Page render ───────────────────────────────────\n const pageStart = 95;\n const pageOpacity = interpolate(frame, [pageStart, pageStart + 12], [0, 1], {\n extrapolateLeft: \"clamp\",\n extrapolateRight: \"clamp\",\n });\n\n // ── Page scroll & cursor motion ───────────────────\n const scrollStart = 115;\n const scrollEnd = 230;\n const scrollT = interpolate(frame, [scrollStart, scrollEnd], [0, 1], {\n extrapolateLeft: \"clamp\",\n extrapolateRight: \"clamp\",\n });\n const scrollEased = easeInOut(scrollT);\n const maxScroll = 600;\n const scrollY = scrollEased * maxScroll;\n\n // Cursor path: starts off the address bar area, moves down toward CTA inside viewport\n // Coordinates relative to the full 1280x720 frame.\n const cursorStartX = browserLeft + 380;\n const cursorStartY = browserTop + chromeHeight + 60;\n const cursorEndX = browserLeft + browserWidth / 2;\n const cursorEndY = browserTop + chromeHeight + 380;\n\n const cursorT = interpolate(frame, [pageStart + 5, scrollEnd], [0, 1], {\n extrapolateLeft: \"clamp\",\n extrapolateRight: \"clamp\",\n });\n const cursorEased = easeInOut(cursorT);\n // arc bias on Y for organic feel\n const arcOffset = Math.sin(cursorEased * Math.PI) * 30;\n const cursorX =\n cursorStartX + (cursorEndX - cursorStartX) * cursorEased + arcOffset * 0.6;\n const cursorY =\n cursorStartY + (cursorEndY - cursorStartY) * cursorEased - arcOffset;\n\n // Click animation near the end\n const clickFrame = scrollEnd + 4;\n const clickSpring = spring({\n frame: frame - clickFrame,\n fps,\n config: { mass: 0.4, damping: 12, stiffness: 220 },\n durationInFrames: 18,\n });\n const cursorClickScale = interpolate(clickSpring, [0, 0.4, 1], [1, 0.85, 1], {\n extrapolateRight: \"clamp\",\n });\n const buttonClickScale =\n frame >= clickFrame\n ? interpolate(clickSpring, [0, 0.4, 1], [1, 0.98, 1], {\n extrapolateRight: \"clamp\",\n })\n : 1;\n\n // Address bar focus state (highlighted while typing)\n const addressFocused = frame < 50;\n\n return (\n \n {/* Browser window */}\n \n {/* Chrome */}\n \n \n {/* nav arrows */}\n \n \n \n \n {/* address bar */}\n \n \n ⌕\n \n {typedUrl}\n {showCaret && (\n \n )}\n\n {/* Autocomplete suggestion dropdown */}\n {showSuggestion && (\n \n \n \n {typedUrl}\n \n {suggestion.slice(typedChars)}\n \n \n \n )}\n \n
\n\n {/* Progress bar */}\n \n \n
\n \n\n {/* Page viewport */}\n \n \n {/* Header */}\n \n \n remocn\n \n \n Components\n Docs\n GitHub\n \n \n\n {/* Hero */}\n \n \n shadcn registry for Remotion\n \n \n Production-ready video blocks.\n \n \n Drop-in animations, transitions, and backgrounds for Remotion.\n Own your code.\n \n\n {/* CTA — clicked at the end */}\n = clickFrame\n ? \"0 0 0 6px rgba(255,255,255,0.08)\"\n : \"0 10px 30px rgba(255,255,255,0.1)\",\n willChange: \"transform\",\n }}\n >\n Browse components →\n \n \n\n {/* Sections */}\n {[\n { title: \"Typography\", color: \"#3b82f6\" },\n { title: \"Backgrounds\", color: \"#a855f7\" },\n { title: \"Transitions\", color: \"#22c55e\" },\n ].map((s) => (\n \n \n \n \n {s.title}\n \n \n Hand-tuned primitives, copied straight into your project.\n \n \n \n ))}\n \n \n \n\n {/* Virtual cursor */}\n {frame >= pageStart && (\n \n \n \n )}\n \n );\n}\n", "type": "registry:component", "target": "components/remocn/browser-flow.tsx" } ], "type": "registry:component" }