Submatter/apps/web/components/marketing/scramble-text.tsx
Randall Stillwell 6ccb060bea feat(second-brain): open self-hosted Obsidian for crews — harness-agnostic v0.1
- MCP create-backlog-item for natural language Plan/Epic/Task authoring
- Worker non-code: optional worktree + per-harness allowlist + doc_review→published
- Harness registry: harness_providers/role_harness_routes, WorkerAdapter (minimal|crewai|langgraph|remote-ag-ui), AG-UI POST /api/ag-ui, Settings → Harnesses with Test
- Plans tree: backlog.listTree + createPlan/Epic/Task + Start→Convoy, web CRUD without CLI
- Fleet: /fleet rollup across workspaces + SSE /api/fleet/events

Decision: docs/decisions/2026-08-25-second-brain-os-pivot.md
Convoy: .convoys/second-brain-os-v01

Co-authored-by: orchestrator
2026-08-25 16:20:53 -05:00

64 lines
1.6 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
const CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/_-";
type ScrambleTextProps = {
text: string;
className?: string;
durationMs?: number;
};
export function ScrambleText({ text, className, durationMs = 1200 }: ScrambleTextProps) {
const reduceMotion = useReducedMotion();
const [display, setDisplay] = useState(text);
useEffect(() => {
if (reduceMotion) {
setDisplay(text);
return;
}
let frame = 0;
const totalFrames = Math.ceil(durationMs / 32);
const locked = new Set<number>();
const id = window.setInterval(() => {
frame += 1;
const progress = frame / totalFrames;
for (let i = 0; i < text.length; i++) {
if (text[i] === " ") continue;
if (Math.random() < progress * 0.15 + 0.05) locked.add(i);
}
const next = text
.split("")
.map((char, i) => {
if (char === " ") return " ";
if (locked.has(i) || progress > 0.92) return char;
return CHARSET[Math.floor(Math.random() * CHARSET.length)] ?? char;
})
.join("");
setDisplay(next);
if (frame >= totalFrames) {
window.clearInterval(id);
setDisplay(text);
}
}, 32);
return () => window.clearInterval(id);
}, [text, durationMs, reduceMotion]);
return (
<motion.span className={cn("inline-block", className)} initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
{display}
</motion.span>
);
}