Files
inkreach-agent/docs/ui-ref/beautifului/loading-state.json
T

18 lines
5.8 KiB
JSON

{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "loading-state",
"type": "registry:component",
"title": "Loading State",
"description": "Pixel-grid loader with shimmer and elapsed time.",
"registryDependencies": [
"https://www.beautifului.dev/r/foundation.json"
],
"files": [
{
"path": "components/primitives/LoadingState.tsx",
"content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * LOADING STATE — pixel-grid loader for long-running work\n *\n * Variants:\n * Drive — square cells, chevron wavefront driving right;\n * the 650ms cycle is shorter than the sweep, so\n * two fronts are always in flight\n * Dots — same wavefront, circular cells\n * Orbit — a comet lapping the grid perimeter\n * Surfer — the Drive loader paired with a meme video below\n *\n * Paired with a shimmering label and a live elapsed timer\n * in mono tabular figures. Reduced motion freezes the grid\n * to its dim state; the timer still ticks.\n * ───────────────────────────────────────────────────────── */\n\nconst chevron = Array.from({ length: 9 }, (_, i) => {\n const r = Math.floor(i / 3), c = i % 3;\n return (c + Math.abs(r - 1)) * 90;\n});\n\nconst ORBIT_ORDER = [0, 1, 2, 5, 8, 7, 6, 3];\nconst orbit = Array.from({ length: 9 }, (_, i) => {\n const k = ORBIT_ORDER.indexOf(i);\n return k === -1 ? null : k * 110;\n});\n\nconst PATTERNS: Record<string, { delays: (number | null)[]; dur: number; round: boolean }> = {\n Drive: { delays: chevron, dur: 650, round: false },\n Dots: { delays: chevron, dur: 650, round: true },\n Orbit: { delays: orbit, dur: 950, round: false },\n};\n\nfunction LoaderGrid({\n delays,\n dur,\n round,\n}: {\n delays: (number | null)[];\n dur: number;\n round: boolean;\n}) {\n return (\n <span aria-hidden className=\"grid shrink-0 grid-cols-[repeat(3,4px)] gap-[1.5px]\">\n {delays.map((delay, index) => (\n <span\n key={index}\n className={`size-[4px] bg-ink ${round ? \"rounded-full\" : \"rounded-[1px]\"}`}\n style={{\n opacity: delay === null ? 0.07 : 0.15,\n animation: delay === null ? \"none\" : `pixel-on ${dur}ms ease-in-out ${delay}ms infinite`,\n }}\n />\n ))}\n </span>\n );\n}\n\nfunction useElapsed() {\n const [ds, setDs] = useState(0);\n useEffect(() => {\n const t = setInterval(() => setDs((d) => d + 1), 100);\n return () => clearInterval(t);\n }, []);\n const total = ds / 10;\n if (total < 60) return `${total.toFixed(1)}s`;\n return `${Math.floor(total / 60)}m ${(total % 60).toFixed(1)}s`;\n}\n\nexport default function LoadingState({\n label,\n variant = \"Drive\",\n /** the meme feed for the Surfer variant; hosted on Vercel Blob so it plays in\n * production (the local /public/subway-surfers.mp4 stays gitignored) */\n videoSrc = \"https://95dnc2a95qgwt9ff.public.blob.vercel-storage.com/subway-surfers.mp4\",\n}: {\n label?: string;\n variant?: string;\n videoSrc?: string;\n}) {\n const elapsed = useElapsed();\n const surfer = variant === \"Surfer\";\n const resolvedLabel = label ?? (surfer ? \"Subway surfing\" : \"Churning\");\n const [videoOk, setVideoOk] = useState(true);\n const { delays, dur, round } = PATTERNS[variant] ?? PATTERNS.Drive;\n\n const labelEl = (\n <span\n className=\"bg-clip-text text-[13px] font-medium text-transparent\"\n style={{\n backgroundImage:\n \"linear-gradient(90deg, var(--ink-3) 35%, var(--ink) 50%, var(--ink-3) 65%)\",\n backgroundSize: \"200% 100%\",\n animation: \"shimmer-text 1.4s linear infinite\",\n }}\n >\n {resolvedLabel}\n </span>\n );\n const elapsedEl = <span className=\"font-mono text-[12px] text-ink-3 tabular-nums\">{elapsed}</span>;\n\n if (surfer) {\n return (\n <div role=\"status\" className=\"flex w-fit flex-col items-start\">\n <div className=\"flex items-center gap-2.5\">\n <LoaderGrid {...PATTERNS.Drive} />\n {labelEl}\n {elapsedEl}\n </div>\n\n {/* the context card follows the status text it is illustrating */}\n <div\n className=\"mt-2 w-56 overflow-hidden rounded-[10px] shadow-overlay\"\n style={{ animation: \"pop-in 200ms cubic-bezier(0.16,1,0.3,1) both\", transformOrigin: \"top left\" }}\n >\n <div className=\"relative aspect-video w-full\" style={{ background: \"var(--tooltip-bg)\" }}>\n {videoOk ? (\n <video\n src={videoSrc}\n autoPlay\n muted\n loop\n playsInline\n onError={() => setVideoOk(false)}\n className=\"h-full w-full object-cover\"\n />\n ) : (\n <div className=\"flex h-full w-full flex-col items-center justify-center gap-1.5\">\n <LoaderGrid {...PATTERNS.Drive} />\n <span className=\"px-3 text-center font-mono text-[10px]\" style={{ color: \"var(--tooltip-muted)\" }}>\n Video unavailable\n </span>\n </div>\n )}\n </div>\n </div>\n </div>\n );\n }\n\n return (\n <div role=\"status\" className=\"flex w-fit items-center gap-2.5\">\n <LoaderGrid delays={delays} dur={dur} round={round} />\n {labelEl}\n {elapsedEl}\n </div>\n );\n}\n",
"type": "registry:component",
"target": "components/primitives/LoadingState.tsx"
}
]
}