{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "terminal",
  "title": "Terminal",
  "description": "Command lines as real DOM text inside an ASCII frame, with an optional typing effect.",
  "dependencies": [],
  "registryDependencies": [
    "https://dotgrain.space/r/engine.json"
  ],
  "files": [
    {
      "path": "registry/ui/terminal.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useState, useSyncExternalStore, type CSSProperties, type Ref } from \"react\";\nimport { GLYPH_ASPECT } from \"@/lib/dotgrain/glyphs\";\nimport { paletteColors } from \"@/lib/dotgrain/palettes\";\nimport { useColorScheme, useDitherFont, useDitherTokens, useReducedMotion } from \"@/lib/dotgrain/provider\";\nimport { Quantize } from \"@/lib/dotgrain/quantize\";\nimport type { DitherTokens } from \"@/lib/dotgrain/types\";\n\nexport type TerminalProps = Partial<Omit<DitherTokens, \"mode\" | \"pattern\">> & {\n  /** One string per line, shown as real DOM text. */\n  lines: string[];\n  /** Type the lines out on mount; the full text is in the DOM throughout. */\n  typing?: boolean;\n  /** Characters per second at speed 1. */\n  rate?: number;\n  className?: string;\n  style?: CSSProperties;\n  ref?: Ref<HTMLDivElement>;\n};\n\nconst subscribe = () => () => {};\n\nexport function Terminal({ lines, typing = false, rate = 40, className, style, ref, ...tokens }: TerminalProps) {\n  const { cell, speed, palette } = useDitherTokens({ ...tokens, mode: \"ascii\" });\n  const colors = paletteColors(palette, useColorScheme().scheme);\n  const font = useDitherFont() ?? \"ui-monospace, monospace\";\n  const reduced = useReducedMotion();\n  // False on the server and during hydration, so server HTML always shows every line.\n  const hydrated = useSyncExternalStore(subscribe, () => true, () => false);\n  const total = lines.join(\"\").length;\n  const animate = typing && hydrated && !reduced && speed > 0;\n  const [shown, setShown] = useState(0);\n\n  useEffect(() => {\n    if (!animate) return;\n    let n = 0;\n    const id = setInterval(() => {\n      setShown((n = Math.min(total, n + 1)));\n      if (n >= total) clearInterval(id);\n    }, 1000 / (rate * speed));\n    return () => clearInterval(id);\n  }, [animate, total, rate, speed]);\n\n  const budget = animate ? shown : total;\n  const pad = `${cell * 2}px ${Math.round(cell * GLYPH_ASPECT * 3)}px`;\n  return (\n    <div ref={ref} className={className} style={{ position: \"relative\", padding: pad, fontFamily: font, whiteSpace: \"pre-wrap\", color: colors[colors.length - 1], ...style }}>\n      <Quantize source={{ kind: \"field\", field: \"frame\", params: { border: 1 } }} {...tokens} mode=\"ascii\" style={{ position: \"absolute\", inset: 0 }} />\n      {lines.map((line, i) => {\n        const typed = line.slice(0, Math.max(0, budget - lines.slice(0, i).join(\"\").length));\n        return (\n          // Untyped characters stay in the DOM, only transparent, so screen readers and layout get the full text.\n          <div key={i} style={{ position: \"relative\", minHeight: \"1lh\" }}>\n            {typed}\n            {typed.length < line.length && <span style={{ color: \"transparent\" }}>{line.slice(typed.length)}</span>}\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "modes": [
      "ascii"
    ]
  },
  "categories": [
    "surface"
  ],
  "type": "registry:ui"
}