{
"$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 ??