{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glyph-field",
  "title": "字符矩阵",
  "description": "由主题色字符组成的自适应画布背景，支持混合色调、渐隐遮罩、中心内容与离屏暂停。",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "https://yiui.dev/r/flow-tone.json"
  ],
  "files": [
    {
      "path": "src/registry/ui/glyph-field/glyph-field.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { FLOW_TONES, type FlowTone } from \"@/lib/flow-tone\"\n\nconst GLYPHS = {\n  latin: \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\",\n  binary: \"01\",\n  hex: \"0123456789ABCDEF\",\n  katakana: \"アイウエオカキクケコサシスセソタチツテトナニヌネノ\",\n  cjk: \"天地山水风云星月日光万物生长安全数据信息连接世界\",\n}\n\ntype GlyphFieldSet = keyof typeof GLYPHS | (string & {})\ntype GlyphFieldProps = React.ComponentProps<\"div\"> & {\n  /** 预设名或自定义字符集。@default \"latin\" */\n  glyphs?: GlyphFieldSet\n  /** 单色或混合色调，颜色取自容器的主题变量。@default \"muted\" */\n  tone?: FlowTone | FlowTone[]\n  /** 格子占用率，范围 0–1。@default 0.8 */\n  density?: number\n  /** 字号，单位 px，最小 8。@default 14 */\n  fontSize?: number\n  /** 每个逻辑帧更新的格子比例，范围 0–1，逻辑帧率 20fps。@default 0.04 */\n  speed?: number\n  /** edges 四周淡出，center 中心留白。@default \"edges\" */\n  fade?: \"edges\" | \"center\" | \"none\"\n  paused?: boolean\n  /** 只描述字符背景；不会隐藏子内容的可访问性。 */\n  label?: string\n}\n\n/**\n * 调整用的覆盖项：子树里所有 `GlyphField` 的对应属性被这里的值替换。\n * 用于调试面板一类的场景；不包 `GlyphFieldTuningContext.Provider` 时组件行为不变。\n */\ntype GlyphFieldTuning = {\n  glyphs?: GlyphFieldSet\n  tone?: FlowTone | FlowTone[]\n  density?: number\n  fontSize?: number\n  speed?: number\n  fade?: \"edges\" | \"center\" | \"none\"\n  paused?: boolean\n  /** 回报示例自己的实际取值（未被覆盖前），面板据此显示当前值。 */\n  onReport?: (values: GlyphFieldReport) => void\n}\n\ntype GlyphFieldReport = {\n  glyphs: GlyphFieldSet\n  tone: FlowTone | FlowTone[]\n  density: number\n  fontSize: number\n  speed: number\n  fade: \"edges\" | \"center\" | \"none\"\n  paused: boolean\n}\n\nconst GlyphFieldTuningContext = React.createContext<GlyphFieldTuning>({})\n\nconst glyphFieldVariants = cva(\"pointer-events-none absolute inset-0\", {\n  variants: {\n    fade: {\n      edges: \"[mask-image:radial-gradient(ellipse,black_25%,transparent_85%)]\",\n      center:\n        \"[mask-image:radial-gradient(ellipse,transparent_35%,black_100%)]\",\n      none: \"\",\n    },\n  },\n  defaultVariants: { fade: \"edges\" },\n})\n\n/** 同尺寸的服务端底色占位与客户端字符画布；子内容保持普通 DOM 语义。 */\nfunction GlyphField({\n  className,\n  children,\n  glyphs: glyphsProp = \"latin\",\n  tone: toneProp = \"muted\",\n  density: densityProp = 0.8,\n  fontSize: fontSizeProp = 14,\n  speed: speedProp = 0.04,\n  fade: fadeProp = \"edges\",\n  paused: pausedProp = false,\n  label,\n  ...props\n}: GlyphFieldProps) {\n  const tuning = React.useContext(GlyphFieldTuningContext)\n  const { onReport } = tuning\n  // 色调数组用值作为依赖，调用者每次传新数组也不会反复回报\n  const reportToneKey = JSON.stringify(toneProp)\n  React.useEffect(() => {\n    onReport?.({\n      glyphs: glyphsProp,\n      tone: JSON.parse(reportToneKey) as FlowTone | FlowTone[],\n      density: densityProp,\n      fontSize: fontSizeProp,\n      speed: speedProp,\n      fade: fadeProp,\n      paused: pausedProp,\n    })\n  }, [\n    onReport,\n    glyphsProp,\n    reportToneKey,\n    densityProp,\n    fontSizeProp,\n    speedProp,\n    fadeProp,\n    pausedProp,\n  ])\n  const glyphs = tuning.glyphs ?? glyphsProp\n  const tone = tuning.tone ?? toneProp\n  const density = tuning.density ?? densityProp\n  const fontSize = tuning.fontSize ?? fontSizeProp\n  const speed = tuning.speed ?? speedProp\n  const fade = tuning.fade ?? fadeProp\n  const paused = tuning.paused ?? pausedProp\n  const canvasRef = React.useRef<HTMLCanvasElement>(null)\n  // 用值作为依赖，避免调用者每次创建新的色调数组而重启画布。\n  const toneKey = JSON.stringify(tone)\n  React.useEffect(() => {\n    const canvas = canvasRef.current\n    const host = canvas?.parentElement\n    const ctx = canvas?.getContext(\"2d\")\n    if (!canvas || !host || !ctx) return\n    const alphabet = Array.from(GLYPHS[glyphs as keyof typeof GLYPHS] ?? glyphs)\n    if (!alphabet.length) return\n    const size = Math.max(8, fontSize)\n    const stepX = size * 1.2\n    const stepY = size * 1.65\n    let columns = 0\n    let cells: { glyph: string; color: number; opacity: number }[] = []\n    let colors: string[] = []\n    let frame = 0\n    let last = 0\n    let visible = false\n    let randomSeed = 421\n    const media = matchMedia(\"(prefers-reduced-motion: reduce)\")\n    const random = () => {\n      randomSeed = (Math.imul(randomSeed, 1664525) + 1013904223) >>> 0\n      return randomSeed / 4294967296\n    }\n    function newCell() {\n      return {\n        glyph:\n          random() <= Math.max(0, Math.min(1, density))\n            ? alphabet[Math.floor(random() * alphabet.length)]!\n            : \"\",\n        color: Math.floor(random() * colors.length),\n        opacity: 0.18 + random() * 0.55,\n      }\n    }\n    function paint(index: number) {\n      if (!ctx) return\n      const x = (index % columns) * stepX\n      const y = Math.floor(index / columns) * stepY\n      ctx.clearRect(x, y, stepX, stepY)\n      const cell = cells[index]\n      if (!cell) return\n      ctx.globalAlpha = cell.opacity\n      ctx.fillStyle = colors[cell.color % colors.length]!\n      ctx.fillText(cell.glyph, x + stepX / 2, y + stepY / 2)\n    }\n    function palette() {\n      if (!host) return\n      const swatch = document.createElement(\"span\")\n      swatch.hidden = true\n      host.append(swatch)\n      const tones = JSON.parse(toneKey) as FlowTone | FlowTone[]\n      colors = (\n        Array.isArray(tones) ? (tones.length ? tones : [\"muted\"]) : [tones]\n      ).map((value) => {\n        swatch.style.color =\n          FLOW_TONES[value as keyof typeof FLOW_TONES] ?? value\n        return getComputedStyle(swatch).color\n      })\n      swatch.remove()\n      cells.forEach((_, index) => paint(index))\n    }\n    function resize() {\n      if (!host || !canvas || !ctx) return\n      const { width, height } = host.getBoundingClientRect()\n      const dpr = Math.min(2, window.devicePixelRatio || 1)\n      canvas.width = Math.round(width * dpr)\n      canvas.height = Math.round(height * dpr)\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n      ctx.font = `${size}px ${getComputedStyle(host).getPropertyValue(\"--font-geist-mono\") || \"monospace\"}`\n      ctx.textAlign = \"center\"\n      ctx.textBaseline = \"middle\"\n      columns = Math.ceil(width / stepX)\n      // 限制超大容器的单次工作量。\n      cells = Array.from(\n        { length: Math.min(30000, columns * Math.ceil(height / stepY)) },\n        newCell,\n      )\n      cells.forEach((_, index) => paint(index))\n    }\n    function tick(now: number) {\n      if (now - last >= 50) {\n        const count = Math.ceil(cells.length * Math.max(0, Math.min(1, speed)))\n        for (let n = 0; n < count; n++) {\n          const index = Math.floor(random() * cells.length)\n          if (!cells.length) break\n          cells[index] = newCell()\n          paint(index)\n        }\n        last = now\n      }\n      frame = requestAnimationFrame(tick)\n    }\n    function sync() {\n      cancelAnimationFrame(frame)\n      if (visible && !document.hidden && !paused && !media.matches && speed > 0)\n        frame = requestAnimationFrame(tick)\n    }\n    palette()\n    resize()\n    const resizeObserver = new ResizeObserver(resize)\n    resizeObserver.observe(host)\n    const intersection = new IntersectionObserver(([entry]) => {\n      visible = entry?.isIntersecting ?? false\n      sync()\n    })\n    intersection.observe(host)\n    const mutations = new MutationObserver(palette)\n    // 只观察祖先属性，避免画布或探针引起反馈循环，并支持局部主题。\n    for (\n      let ancestor: Element | null = host;\n      ancestor;\n      ancestor = ancestor.parentElement\n    ) {\n      mutations.observe(ancestor, {\n        attributes: true,\n        attributeFilter: [\"class\", \"data-theme\", \"style\"],\n      })\n    }\n    media.addEventListener(\"change\", sync)\n    document.addEventListener(\"visibilitychange\", sync)\n    window.addEventListener(\"resize\", resize)\n    return () => {\n      cancelAnimationFrame(frame)\n      resizeObserver.disconnect()\n      intersection.disconnect()\n      mutations.disconnect()\n      media.removeEventListener(\"change\", sync)\n      document.removeEventListener(\"visibilitychange\", sync)\n      window.removeEventListener(\"resize\", resize)\n    }\n  }, [glyphs, toneKey, density, fontSize, speed, paused])\n\n  return (\n    <div\n      data-slot=\"glyph-field\"\n      data-paused={paused || undefined}\n      className={cn(\n        \"relative isolate flex min-h-72 w-full items-center justify-center overflow-hidden bg-background\",\n        className,\n      )}\n      {...props}\n    >\n      <div\n        data-slot=\"glyph-field-background\"\n        role={label ? \"img\" : undefined}\n        aria-label={label}\n        aria-hidden={label ? undefined : true}\n        className={glyphFieldVariants({ fade })}\n      >\n        <div\n          data-slot=\"glyph-field-placeholder\"\n          className=\"absolute inset-0 bg-muted/20\"\n        />\n        <canvas\n          ref={canvasRef}\n          data-slot=\"glyph-field-canvas\"\n          className=\"absolute inset-0 size-full\"\n        />\n      </div>\n      <div\n        data-slot=\"glyph-field-content\"\n        className=\"relative z-10 flex max-w-full flex-col items-center justify-center text-center\"\n      >\n        {children}\n      </div>\n    </div>\n  )\n}\n\nexport { GlyphField, GlyphFieldTuningContext, glyphFieldVariants }\nexport type {\n  GlyphFieldProps,\n  GlyphFieldReport,\n  GlyphFieldSet,\n  GlyphFieldTuning,\n}\n",
      "type": "registry:ui",
      "target": "@ui/glyph-field/glyph-field.tsx"
    }
  ],
  "type": "registry:ui"
}
