{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "changelog-bite", "title": "Changelog Bite", "description": "Seamlessly looping square card showing a before/after diff with a frosted glass wipe and a pulsing New pill.", "dependencies": [ "remotion" ], "files": [ { "path": "registry/remocn/changelog-bite/index.tsx", "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport {\n AbsoluteFill,\n Easing,\n interpolate,\n spring,\n useCurrentFrame,\n useVideoConfig,\n} from \"remotion\";\n\nexport interface ChangelogBiteProps {\n label?: string;\n title?: string;\n oldContent?: ReactNode;\n newContent?: ReactNode;\n format?: \"square\" | \"portrait\";\n background?: string;\n cardBackground?: string;\n accent?: 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\nconst SOFT = Easing.bezier(0.16, 1, 0.3, 1);\n\nexport function ChangelogBite({\n label = \"New\",\n title = \"Inline diff view\",\n oldContent,\n newContent,\n format = \"square\",\n background = \"#141318\",\n cardBackground = \"rgba(20, 19, 24, 0.92)\",\n accent = \"#FFB38E\",\n speed = 1,\n className,\n}: ChangelogBiteProps) {\n const frame = useCurrentFrame() * speed;\n const { fps, durationInFrames } = useVideoConfig();\n\n // ── Card scale/opacity envelope (loop seam) ─────────────────────────────\n // First 10 frames: 0.95 → 1.0, 0 → 1\n // Last 15 frames: 1.0 → 0.95, 1 → 0\n // The match means a flat-cut from final to first frame is invisible.\n const enterT = interpolate(frame, [0, 10], [0, 1], {\n extrapolateLeft: \"clamp\",\n extrapolateRight: \"clamp\",\n easing: SOFT,\n });\n const exitT = interpolate(\n frame,\n [durationInFrames - 15, durationInFrames - 1],\n [0, 1],\n { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\", easing: SOFT },\n );\n const cardScale = 0.95 + enterT * 0.05 - exitT * 0.05;\n const cardOpacity = enterT * (1 - exitT);\n\n // ── Wipe progress (frosted glass + clip) ────────────────────────────────\n const wipe = spring({\n frame: frame - 60,\n fps,\n config: { damping: 18, mass: 0.9, stiffness: 90 },\n durationInFrames: 50,\n });\n const wipePct = wipe * 100;\n\n // ── Aspect-driven inner card sizing ─────────────────────────────────────\n const isSquare = format === \"square\";\n const cardWidth = isSquare ? 880 : 880;\n const cardHeight = isSquare ? 880 : 1100;\n\n const oldNode = oldContent ?? ;\n const newNode = newContent ?? ;\n\n return (\n \n \n {/* Header row: title + New pill */}\n \n \n {title}\n \n \n \n\n {/* Diff stage */}\n \n {/* New (bottom layer) */}\n
{newNode}
\n\n {/* Old (top layer, clipped left-to-right) */}\n \n {oldNode}\n \n\n {/* Frosted glass shutter sweeping over the seam */}\n {wipePct > 0 && wipePct < 100 && (\n \n )}\n\n {/* Subtle accent seam line */}\n {wipePct > 0 && wipePct < 100 && (\n \n )}\n \n\n {/* Footer caption — labels swap mid-wipe */}\n \n before\n after\n \n \n \n );\n}\n\n/* -------------------------------------------------------------------------- */\n/* Default content */\n/* -------------------------------------------------------------------------- */\n\nfunction DefaultOldPanel({ accent }: { accent: string }) {\n return (\n \n
{\"function diff(a, b) {\"}
\n
{\" if (a === b) return null;\"}
\n
{\" return {\"}
\n
{\" before: a,\"}
\n
{\" after: b,\"}
\n
{\" };\"}
\n
{\"}\"}
\n \n BEFORE\n \n {/* swallow lint */}\n {accent}\n \n );\n}\n\nfunction DefaultNewPanel({ accent }: { accent: string }) {\n return (\n \n
\n function{\" \"}\n diff\n {\"(a, b) {\"}\n
\n
\n {\" return \"}\n visualDiff\n {\"(a, b);\"}\n
\n
{\"}\"}
\n \n AFTER\n \n \n );\n}\n\n/* -------------------------------------------------------------------------- */\n/* Pulsing pill */\n/* -------------------------------------------------------------------------- */\n\nfunction PulsingPill({\n label,\n color,\n speed,\n}: {\n label: string;\n color: string;\n speed: number;\n}) {\n const frame = useCurrentFrame() * speed;\n const wave = (Math.sin(frame / 8) + 1) / 2;\n const dotScale = 0.92 + wave * 0.16;\n const ringPhase = (frame % 60) / 60;\n const ringScale = 1 + ringPhase * 1.6;\n const ringOpacity = (1 - ringPhase) * 0.6;\n\n return (\n \n
\n \n \n
\n {label}\n \n );\n}\n", "type": "registry:component", "target": "components/remocn/changelog-bite.tsx" } ], "type": "registry:component" }