{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "braille-loader",
  "type": "registry:ui",
  "title": "Braille Loader",
  "description": "Accessible braille-inspired loading indicator with 25 animation variants.",
  "files": [
    {
      "path": "registry/new-york/ui/braille-loader.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport {\n  type BrailleLoaderSpeed,\n  type BrailleLoaderVariant,\n  generateFrames,\n  getVariantGridSize,\n  normalizeVariant,\n} from \"@/lib/braille-loader\";\n\ntype BrailleLoaderProps = React.ComponentProps<\"div\"> & {\n  variant?: BrailleLoaderVariant;\n  speed?: BrailleLoaderSpeed;\n  label?: string;\n  fontSize?: number;\n};\n\nconst speedMultiplier: Record<BrailleLoaderSpeed, number> = {\n  slow: 1.5,\n  normal: 1,\n  fast: 0.6,\n};\n\nfunction getPrefersReducedMotion() {\n  return (\n    typeof window !== \"undefined\" &&\n    \"matchMedia\" in window &&\n    window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n  );\n}\n\nfunction usePrefersReducedMotion() {\n  const [prefersReducedMotion, setPrefersReducedMotion] = React.useState(getPrefersReducedMotion);\n\n  React.useEffect(() => {\n    if (!(\"matchMedia\" in window)) return;\n\n    const mediaQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const handleChange = () => setPrefersReducedMotion(mediaQuery.matches);\n\n    handleChange();\n    mediaQuery.addEventListener(\"change\", handleChange);\n\n    return () => {\n      mediaQuery.removeEventListener(\"change\", handleChange);\n    };\n  }, []);\n\n  return prefersReducedMotion;\n}\n\nfunction BrailleLoader({\n  variant = \"breathe\",\n  speed = \"normal\",\n  className,\n  label = \"Loading\",\n  fontSize = 28,\n  style,\n  ...props\n}: BrailleLoaderProps) {\n  const resolvedVariant = normalizeVariant(variant);\n  const [width, height] = getVariantGridSize(resolvedVariant);\n  const spanRef = React.useRef<HTMLSpanElement>(null);\n  const prefersReducedMotion = usePrefersReducedMotion();\n\n  const framesData = React.useMemo(() => {\n    return generateFrames(resolvedVariant, width, height);\n  }, [resolvedVariant, width, height]);\n\n  React.useEffect(() => {\n    if (prefersReducedMotion || !spanRef.current) return;\n\n    const frames = framesData.frames;\n    let frameIndex = 0;\n    const baseInterval = framesData.interval;\n    const interval = baseInterval * speedMultiplier[speed];\n\n    const updateFrame = () => {\n      if (spanRef.current) {\n        spanRef.current.textContent = frames[frameIndex];\n      }\n      frameIndex = (frameIndex + 1) % frames.length;\n    };\n\n    updateFrame();\n    const intervalId = setInterval(updateFrame, interval);\n\n    return () => {\n      clearInterval(intervalId);\n    };\n  }, [framesData, prefersReducedMotion, speed]);\n\n  return (\n    <div\n      role=\"status\"\n      aria-live=\"polite\"\n      className={[\"inline-flex items-center text-current\", className].filter(Boolean).join(\" \")}\n      style={style}\n      {...props}\n    >\n      <span className=\"sr-only\">{label}</span>\n      <span\n        ref={spanRef}\n        aria-hidden=\"true\"\n        style={{\n          fontFamily: \"monospace\",\n          whiteSpace: \"pre\",\n          fontSize: `${fontSize}px`,\n          lineHeight: 1,\n          letterSpacing: 0,\n        }}\n      >\n        {framesData.frames[0]}\n      </span>\n    </div>\n  );\n}\n\nexport { BrailleLoader, type BrailleLoaderProps };\n",
      "type": "registry:ui",
      "target": "@ui/braille-loader.tsx"
    },
    {
      "path": "lib/braille-loader.ts",
      "content": "export const brailleLoaderVariants = [\n  \"breathe\",\n  \"pulse\",\n  \"orbit\",\n  \"snake\",\n  \"fill-sweep\",\n  \"scan\",\n  \"rain\",\n  \"cascade\",\n  \"checkerboard\",\n  \"columns\",\n  \"wave-rows\",\n  \"diagonal-swipe\",\n  \"sparkle\",\n  \"helix\",\n  \"braille\",\n  \"reflected-ripple\",\n  \"pendulum\",\n  \"compress\",\n  \"sort\",\n  \"equalizer\",\n  \"chase\",\n  \"bars\",\n  \"marquee\",\n  \"typing\",\n  \"spiral\",\n] as const;\n\nexport type BrailleLoaderVariant = (typeof brailleLoaderVariants)[number];\nexport type BrailleLoaderSpeed = \"slow\" | \"normal\" | \"fast\";\n\nexport const speedToDuration: Record<BrailleLoaderSpeed, number> = {\n  slow: 3000,\n  normal: 2400,\n  fast: 1200,\n};\n\nconst DOT_BITS = [\n  [0x01, 0x08],\n  [0x02, 0x10],\n  [0x04, 0x20],\n  [0x40, 0x80],\n];\n\nconst BRAILLE_BASE = 0x2800;\n\nfunction clamp(value: number, min: number, max: number): number {\n  return Math.min(max, Math.max(min, value));\n}\n\nexport function seededRandom(seed: number): () => number {\n  let s = seed;\n  return () => {\n    s = (s * 1664525 + 1013904223) & 0xffffffff;\n    return (s >>> 0) / 0xffffffff;\n  };\n}\n\nfunction smoothstep(t: number): number {\n  return t * t * (3 - 2 * t);\n}\n\nfunction setDot(brailleChar: number, row: number, col: number): number {\n  if (row < 0 || row > 3) return brailleChar;\n  return brailleChar | DOT_BITS[row][col];\n}\n\nfunction createFieldBuffer(width: number): number[] {\n  return Array.from({ length: width }, () => 0);\n}\n\nfunction fieldToString(field: number[]): string {\n  return field.map((mask) => String.fromCharCode(BRAILLE_BASE + mask)).join(\"\");\n}\n\ntype VariantConfig = {\n  totalFrames: number;\n  interval: number;\n  gridSize: [number, number];\n  compute: (frame: number, totalFrames: number, width: number, height: number, context: PrecomputeContext) => number[];\n};\n\ntype PrecomputeContext = {\n  importance: number[];\n  shuffled: number[];\n  target: number[];\n  colRandom: number[];\n};\n\nconst contextCache = new Map<string, PrecomputeContext>();\n\nexport function getPrecomputeContext(width: number, height: number): PrecomputeContext {\n  const key = `${width}x${height}`;\n  let ctx = contextCache.get(key);\n  if (!ctx) {\n    const pixelCols = width * 2;\n    const totalDots = pixelCols * height;\n\n    const rand42 = seededRandom(42);\n    const importance = Array.from({ length: totalDots }, () => rand42());\n\n    const rand19 = seededRandom(19);\n    const shuffled: number[] = [];\n    const target: number[] = [];\n    for (let i = 0; i < pixelCols; i++) {\n      shuffled.push(rand19() * (height - 1));\n      target.push((1 - i / (pixelCols - 1)) * (height - 1));\n    }\n\n    const rand123 = seededRandom(123);\n    const colRandom: number[] = [];\n    for (let pc = 0; pc < pixelCols; pc++) {\n      colRandom.push(rand123());\n    }\n\n    ctx = {\n      importance,\n      shuffled,\n      target,\n      colRandom,\n    };\n    contextCache.set(key, ctx);\n  }\n  return ctx;\n}\n\nexport const VARIANT_CONFIGS: Record<string, VariantConfig> = {\n  pendulum: {\n    totalFrames: 120,\n    interval: 12,\n    gridSize: [5, 4],\n\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const field = createFieldBuffer(width);\n\n      const progress = frame / totalFrames;\n      const pixelCols = width * 2;\n\n      // ✅ fast natural swing\n      const basePhase = progress * Math.PI * 8;\n\n      // ✅ dynamic spatial sine (CRITICAL)\n      const spread = Math.sin(progress * Math.PI) * 1.1;\n\n      const threshold = 0.7;\n\n      for (let pc = 0; pc < pixelCols; pc++) {\n        // ⭐ sine exists INSIDE braille cell\n        const swing = Math.sin(basePhase + pc * spread);\n\n        const center = ((1 - swing) * (height - 1)) / 2;\n\n        for (let row = 0; row < height; row++) {\n          if (Math.abs(row - center) < threshold) {\n            const charIdx = Math.floor(pc / 2);\n            field[charIdx] = setDot(field[charIdx], row, pc % 2);\n          }\n        }\n      }\n\n      return field;\n    },\n  },\n  compress: {\n    totalFrames: 100,\n    interval: 40,\n    gridSize: [5, 5],\n    compute: (frame, totalFrames, width, height, ctx) => {\n      const progress = frame / totalFrames;\n      const sieveThreshold = Math.max(0.1, 1 - progress * 1.2);\n      const squeeze = Math.min(1, progress / 0.85);\n      const activeWidth = Math.max(1, width * 2 * (1 - squeeze * 0.95));\n      const field = createFieldBuffer(width);\n\n      for (let pc = 0; pc < width * 2; pc++) {\n        const mappedPc = (pc / (width * 2)) * activeWidth;\n        if (mappedPc >= activeWidth) continue;\n        const targetPc = Math.round(mappedPc);\n        if (targetPc >= width * 2) continue;\n        const charIdx = Math.floor(targetPc / 2);\n        const dc = targetPc % 2;\n\n        for (let row = 0; row < height; row++) {\n          const importanceIdx = pc * height + row;\n          if (ctx.importance[importanceIdx] < sieveThreshold) {\n            field[charIdx] = setDot(field[charIdx], row, dc);\n          }\n        }\n      }\n      return field;\n    },\n  },\n\n  sort: {\n    totalFrames: 100,\n    interval: 40,\n    gridSize: [5, 6],\n\n    compute: (frame, totalFrames, width, height, ctx) => {\n      const progress = frame / totalFrames;\n      const pixelCols = width * 2;\n\n      const field = createFieldBuffer(width);\n\n      // sorting front\n      const cursor = progress * pixelCols;\n\n      for (let pc = 0; pc < pixelCols; pc++) {\n        const charIdx = Math.floor(pc / 2);\n        const dc = pc % 2;\n\n        let fillHeight;\n\n        // =========================\n        // ✅ SORTED REGION\n        // =========================\n        if (pc < cursor - 1) {\n          fillHeight = ctx.target[pc];\n        }\n\n        // =========================\n        // ✅ ACTIVE FRONT\n        // =========================\n        else if (Math.abs(pc - cursor) < 2) {\n          const blend = 1 - Math.abs(pc - cursor) / 2;\n\n          const ease = blend * blend * (3 - 2 * blend);\n\n          fillHeight = ctx.shuffled[pc] + (ctx.target[pc] - ctx.shuffled[pc]) * ease;\n\n          // flash = comparison\n          if (blend > 0.7) {\n            for (let r = 0; r < height; r++) {\n              field[charIdx] = setDot(field[charIdx], r, dc);\n            }\n            continue;\n          }\n        }\n\n        // =========================\n        // ✅ UNSORTED REGION\n        // =========================\n        else {\n          fillHeight = ctx.shuffled[pc] + Math.sin(progress * Math.PI * 12 + pc * 2.3) * 0.8;\n        }\n\n        fillHeight = Math.max(0, Math.min(height - 1, fillHeight));\n\n        // ✅ IMPORTANT FIX:\n        // cumulative stacking (NO ERASURE)\n        for (let r = Math.floor(fillHeight); r < height; r++) {\n          field[charIdx] = setDot(field[charIdx], r, dc);\n        }\n      }\n\n      return field;\n    },\n  },\n\n  breathe: {\n    totalFrames: 40,\n    interval: 40,\n    gridSize: [1, 6],\n\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const field = createFieldBuffer(width);\n      const progress = frame / totalFrames;\n\n      const alpha = 0.5 + 0.5 * Math.sin(progress * Math.PI * 2);\n\n      // -----------------------------\n      // loop index (changes ONLY after animation ends)\n      // -----------------------------\n      const loopIndex = Math.floor(frame / totalFrames);\n\n      // -----------------------------\n      // checker dots\n      // -----------------------------\n      const dots: { pc: number; row: number }[] = [];\n\n      for (let pass = 0; pass < 2; pass++) {\n        for (let pc = 0; pc < width * 2; pc++) {\n          for (let row = 0; row < height; row++) {\n            if ((pc + row) % 2 === pass) {\n              dots.push({ pc, row });\n            }\n          }\n        }\n      }\n\n      // -----------------------------\n      // ✅ hole chosen once per loop\n      // -----------------------------\n      const rand = seededRandom(9001 + loopIndex);\n      const holeIndex = Math.floor(rand() * dots.length);\n\n      const maxDots = dots.length - 1;\n      const activeDots = Math.floor(alpha * maxDots);\n\n      let placed = 0;\n\n      for (let i = 0; i < dots.length; i++) {\n        if (i === holeIndex) continue;\n        if (placed >= activeDots) break;\n\n        const { pc, row } = dots[i];\n\n        const charIdx = Math.floor(pc / 2);\n        const dc = pc % 2;\n\n        field[charIdx] = setDot(field[charIdx], row, dc);\n\n        placed++;\n      }\n\n      return field;\n    },\n  },\n\n  pulse: {\n    totalFrames: 23,\n    interval: 60,\n    gridSize: [4, 4],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const period = 900;\n      const t = frame * 40;\n      const scale = 1 + 0.06 * Math.sin((2 * Math.PI * t) / period);\n      const field = createFieldBuffer(width);\n      const centerX = (width * 2 - 1) / 2;\n      const centerY = (height - 1) / 2;\n\n      for (let pc = 0; pc < width * 2; pc++) {\n        for (let row = 0; row < height; row++) {\n          const dx = (pc - centerX) / scale;\n          const dy = (row - centerY) / scale;\n          const dist = Math.sqrt(dx * dx + dy * dy);\n          const maxDist = Math.sqrt(centerX * centerX + centerY * centerY);\n          const ringWidth = 0.8;\n          const ringPos = ((Math.sin(((2 * Math.PI * t) / period) * 2) + 1) / 2) * maxDist;\n\n          if (Math.abs(dist - ringPos) < ringWidth) {\n            const charIdx = Math.floor(pc / 2);\n            const dc = pc % 2;\n            field[charIdx] = setDot(field[charIdx], row, dc);\n          }\n        }\n      }\n      return field;\n    },\n  },\n\n  waveRows: {\n    totalFrames: 20,\n    interval: 40,\n    gridSize: [4, 4],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const progress = frame / totalFrames;\n      const field = createFieldBuffer(width);\n      const pixelCols = width * 2;\n\n      const basePhase = progress * Math.PI * 2;\n      const colPhaseStep = (Math.PI * 2) / Math.max(2, pixelCols);\n      const bandWidth = 0.9;\n\n      for (let pc = 0; pc < pixelCols; pc++) {\n        const colWave = Math.sin(basePhase + pc * colPhaseStep);\n        const centerRow = ((colWave + 1) / 2) * (height - 1);\n\n        for (let row = 0; row < height; row++) {\n          const dist = Math.abs(row - centerRow);\n          if (dist <= bandWidth) {\n            const charIdx = Math.floor(pc / 2);\n            const dc = pc % 2;\n            field[charIdx] = setDot(field[charIdx], row, dc);\n          }\n        }\n      }\n      return field;\n    },\n  },\n\n  snake: {\n    totalFrames: 25,\n    interval: 80,\n    gridSize: [2, 4],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const field = createFieldBuffer(width);\n      const pixelCols = width * 2;\n\n      // Build serpentine path: left→right on even rows, right→left on odd rows\n      const path: Array<{ pc: number; row: number }> = [];\n      for (let row = 0; row < height; row++) {\n        if (row % 2 === 0) {\n          for (let pc = 0; pc < pixelCols; pc++) path.push({ pc, row });\n        } else {\n          for (let pc = pixelCols - 1; pc >= 0; pc--) path.push({ pc, row });\n        }\n      }\n\n      const progress = frame / totalFrames;\n      const trailingCells = 3;\n      const headPos = Math.floor(progress * path.length) % path.length;\n      const deadGap = 0;\n\n      const drawPathDot = (idx: number) => {\n        const { pc, row } = path[idx];\n        const charIdx = Math.floor(pc / 2);\n        const dc = pc % 2;\n        field[charIdx] = setDot(field[charIdx], row, dc);\n      };\n\n      drawPathDot(headPos);\n\n      for (let i = deadGap + 1; i < deadGap + 1 + trailingCells; i++) {\n        const idx = (headPos - i + path.length) % path.length;\n        drawPathDot(idx);\n      }\n\n      return field;\n    },\n  },\n\n  orbit: {\n    totalFrames: 40,\n    interval: 50,\n    gridSize: [4, 4],\n    compute: (frame: number, totalFrames: number, width: number, height: number, _ctx: PrecomputeContext) => {\n      const progress = frame / totalFrames;\n      const field = createFieldBuffer(width);\n\n      // Edge positions in clockwise order around one braille character\n      const edgePositions: Array<{ row: number; dc: number }> = [\n        { row: 0, dc: 0 }, // top-left\n        { row: 0, dc: 1 }, // top-right\n        { row: 1, dc: 1 }, // right-1\n        { row: 2, dc: 1 }, // right-2\n        { row: 3, dc: 1 }, // bottom-right\n        { row: 3, dc: 0 }, // bottom-left\n        { row: 2, dc: 0 }, // left-2\n        { row: 1, dc: 0 }, // left-1\n      ];\n\n      // 3-dot trail moving clockwise\n      const leadPos = Math.floor(progress * edgePositions.length) % edgePositions.length;\n      const trailLength = 3;\n\n      // Only use first character\n      const charIdx = 0;\n\n      for (let i = 0; i < trailLength; i++) {\n        const idx = (leadPos - i + edgePositions.length) % edgePositions.length;\n        const pos = edgePositions[idx];\n\n        if (pos.row < height) {\n          field[charIdx] = setDot(field[charIdx], pos.row, pos.dc);\n        }\n      }\n\n      return field;\n    },\n  },\n\n  rain: {\n    totalFrames: 90,\n    interval: 40,\n    gridSize: [5, 5],\n    compute: (frame, totalFrames, width, height, ctx) => {\n      const field = createFieldBuffer(width);\n      const pixelCols = width * 2;\n      const t = frame * 40;\n      let activeDrops = 0;\n\n      for (let pc = 0; pc < pixelCols; pc++) {\n        const rand = ctx.colRandom[pc] ?? 0;\n\n        const period = 1200 + rand * 1000;\n        const cyclePos = t / period + rand * 0.91 + pc * 0.07;\n        const cycleIndex = Math.floor(cyclePos);\n        const phase = cyclePos - cycleIndex;\n\n        const seedA = cycleIndex * 173 + pc * 37 + Math.floor(rand * 1009);\n        const noiseA = Math.sin(seedA * 12.9898) * 43758.5453;\n        const rollA = noiseA - Math.floor(noiseA);\n\n        const seedB = cycleIndex * 257 + pc * 61 + Math.floor(rand * 881);\n        const noiseB = Math.sin(seedB * 78.233) * 12345.6789;\n        const rollB = noiseB - Math.floor(noiseB);\n\n        const seedC = cycleIndex * 97 + pc * 149 + Math.floor(rand * 733);\n        const noiseC = Math.sin(seedC * 39.3467) * 31337.4242;\n        const rollC = noiseC - Math.floor(noiseC);\n\n        const missChance = 0.02 + rand * 0.08;\n        if (rollA < missChance) continue;\n\n        const spawnDelay = 0.0 + rollB * 0.2;\n        const fallDuration = 0.48 + rollC * 0.42;\n        const endPhase = spawnDelay + fallDuration;\n\n        if (phase < spawnDelay || phase > endPhase) continue;\n\n        const localPhase = (phase - spawnDelay) / fallDuration;\n        const gravityCurve = 1.6 + rollC * 1.2;\n        const accelerated = Math.pow(localPhase, gravityCurve);\n\n        const midWeight = Math.max(0, 1 - Math.abs(localPhase - 0.5) * 2);\n        const wobbleSeed = cycleIndex * 0.73 + pc * 1.31 + rand * 4.7;\n        const midWobble = Math.sin(wobbleSeed + localPhase * Math.PI * 6) * 0.1 * midWeight;\n\n        const y = Math.floor((accelerated + midWobble) * (height + 1)) - 1;\n        if (y < 0 || y >= height) continue;\n\n        const charIdx = Math.floor(pc / 2);\n        const dc = pc % 2;\n        field[charIdx] = setDot(field[charIdx], y, dc);\n        activeDrops++;\n      }\n\n      if (activeDrops === 0) {\n        const fallbackPos = (t / 1600) % 1;\n        const fallbackPc = Math.floor(fallbackPos * pixelCols) % pixelCols;\n        const fallbackPhase = fallbackPos * (height + 1);\n        const fallbackY = clamp(Math.floor(fallbackPhase), 0, height - 1);\n        const fallbackCharIdx = Math.floor(fallbackPc / 2);\n        const fallbackDc = fallbackPc % 2;\n        field[fallbackCharIdx] = setDot(field[fallbackCharIdx], fallbackY, fallbackDc);\n      }\n\n      return field;\n    },\n  },\n\n  sparkle: {\n    totalFrames: 60,\n    interval: 40,\n    gridSize: [5, 5],\n\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const field = createFieldBuffer(width);\n\n      const pixelCols = width * 2;\n      const drawableHeight = Math.min(height, 4);\n\n      /* ---------------------------------- */\n      /* HASH */\n      /* ---------------------------------- */\n      const hash = (x: number, y: number, t: number) => {\n        let n = x * 374761393 + y * 668265263 + t * 1442695041;\n        n = (n ^ (n >> 13)) * 1274126177;\n        return ((n ^ (n >> 16)) >>> 0) / 4294967295;\n      };\n\n      /* ---------------------------------- */\n      /* PARAMETERS */\n      /* ---------------------------------- */\n      const density = 0.095;\n      const lifetime = 4;\n      const phase = Math.floor(frame / 2);\n\n      const regionSize = 2;\n\n      for (let row = 0; row < drawableHeight; row++) {\n        for (let col = 0; col < pixelCols; col++) {\n          let v = hash(col, row, phase);\n\n          /* ---------------------------------- */\n          /* ⭐ EDGE COMPENSATION (NEW) */\n          /* prevents center bias */\n          /* ---------------------------------- */\n          const edgeBias =\n            0.12 * ((col === 0 || col === pixelCols - 1 ? 1 : 0) + (row === 0 || row === drawableHeight - 1 ? 1 : 0));\n\n          v -= edgeBias;\n\n          if (v > density) continue;\n\n          /* ---------------------------------- */\n          /* REGIONAL COMPETITION */\n          /* ---------------------------------- */\n          const rx = Math.floor(col / regionSize);\n          const ry = Math.floor(row / regionSize);\n\n          let winner = true;\n\n          for (let oy = 0; oy < regionSize && winner; oy++) {\n            for (let ox = 0; ox < regionSize; ox++) {\n              const nx = rx * regionSize + ox;\n              const ny = ry * regionSize + oy;\n\n              if (nx === col && ny === row) continue;\n              if (nx >= pixelCols || ny >= drawableHeight) continue;\n\n              let nv = hash(nx, ny, phase);\n\n              const nEdgeBias =\n                0.12 * ((nx === 0 || nx === pixelCols - 1 ? 1 : 0) + (ny === 0 || ny === drawableHeight - 1 ? 1 : 0));\n\n              nv -= nEdgeBias;\n\n              if (nv < v) {\n                winner = false;\n                break;\n              }\n            }\n          }\n\n          if (!winner) continue;\n\n          /* ---------------------------------- */\n          /* LIFECYCLE */\n          /* ---------------------------------- */\n          const offset = Math.floor(hash(col, row, 999) * lifetime);\n\n          const age = (frame + offset) % lifetime;\n\n          if (age > 3) continue;\n\n          /* ---------------------------------- */\n          /* SHIMMER */\n          /* ---------------------------------- */\n          let r = row;\n          let c = col;\n\n          if (hash(col, row, 777) < 0.18 && age === 1) {\n            const dirs = [\n              [-1, -1],\n              [-1, 0],\n              [-1, 1],\n              [0, -1],\n              [0, 1],\n              [1, -1],\n              [1, 0],\n              [1, 1],\n            ];\n\n            const d = dirs[Math.floor(hash(col, row, 555) * dirs.length)];\n\n            r = clamp(r + d[0], 0, drawableHeight - 1);\n            c = clamp(c + d[1], 0, pixelCols - 1);\n          }\n\n          const charIdx = Math.floor(c / 2);\n          const dc = c % 2;\n\n          field[charIdx] = setDot(field[charIdx], r, dc);\n        }\n      }\n\n      return field;\n    },\n  },\n\n  checkerboard: {\n    totalFrames: 60,\n    interval: 40,\n    gridSize: [4, 4],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const drawableHeight = Math.min(height, 4);\n      const phaseFrames = Math.max(2, Math.floor(totalFrames / 8));\n      const phase = Math.floor(frame / phaseFrames) % 2;\n      const field = createFieldBuffer(width);\n\n      for (let pc = 0; pc < width * 2; pc++) {\n        for (let row = 0; row < drawableHeight; row++) {\n          // True checkerboard: alternate both X (pc) and Y (row) positions\n          if ((pc + row) % 2 === phase) {\n            const charIdx = Math.floor(pc / 2);\n            const dc = pc % 2;\n            field[charIdx] = setDot(field[charIdx], row, dc);\n          }\n        }\n      }\n      return field;\n    },\n  },\n\n  columns: {\n    totalFrames: 48,\n    interval: 40,\n    gridSize: [4, 4],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const field = createFieldBuffer(width);\n      const pixelCols = width * 2;\n      const stepsPerColumn = height + 1;\n      const totalSteps = pixelCols * stepsPerColumn;\n\n      const progress = frame / totalFrames;\n      const step = Math.floor(progress * totalSteps) % totalSteps;\n      const activePc = Math.floor(step / stepsPerColumn);\n      const activeFill = step % stepsPerColumn;\n\n      const fillColumnToTop = (pc: number) => {\n        const charIdx = Math.floor(pc / 2);\n        const dc = pc % 2;\n        for (let row = 0; row < height; row++) {\n          field[charIdx] = setDot(field[charIdx], row, dc);\n        }\n      };\n\n      const fillColumnBottomUp = (pc: number, count: number) => {\n        const charIdx = Math.floor(pc / 2);\n        const dc = pc % 2;\n        const dotsToFill = Math.max(0, Math.min(height, count));\n        for (let i = 0; i < dotsToFill; i++) {\n          const row = height - 1 - i;\n          field[charIdx] = setDot(field[charIdx], row, dc);\n        }\n      };\n\n      for (let pc = 0; pc < pixelCols; pc++) {\n        if (pc < activePc) {\n          fillColumnToTop(pc);\n          continue;\n        }\n\n        if (pc === activePc) {\n          fillColumnBottomUp(pc, activeFill);\n        }\n      }\n\n      return field;\n    },\n  },\n\n  cascade: {\n    totalFrames: 60,\n    interval: 40,\n    gridSize: [5, 5],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const progress = frame / totalFrames;\n      const field = createFieldBuffer(width);\n      const leadingEdge = progress * 2;\n\n      for (let pc = 0; pc < width * 2; pc++) {\n        const normalizedX = pc / (width * 2);\n        for (let row = 0; row < height; row++) {\n          const normalizedY = row / height;\n          const diagonalSum = normalizedX + normalizedY;\n          const delta = Math.abs(diagonalSum - leadingEdge);\n\n          if (delta < 0.2) {\n            const charIdx = Math.floor(pc / 2);\n            const dc = pc % 2;\n            field[charIdx] = setDot(field[charIdx], row, dc);\n          }\n        }\n      }\n      return field;\n    },\n  },\n\n  diagonalSwipe: {\n    totalFrames: 60,\n    interval: 30,\n    gridSize: [3, 6],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const field = createFieldBuffer(width);\n      const pixelCols = width * 2;\n\n      const maxDiag = pixelCols - 1 + (height - 1);\n      const cycleFrame = frame % totalFrames;\n      const clearFrames = Math.max(2, Math.floor(totalFrames / 2));\n      const fillFrames = Math.max(2, totalFrames - clearFrames);\n\n      const clearPhase = cycleFrame < clearFrames;\n      const localFrame = clearPhase ? cycleFrame : cycleFrame - clearFrames;\n      const localTotal = (clearPhase ? clearFrames : fillFrames) - 1;\n      const phaseProgress = localTotal > 0 ? localFrame / localTotal : 1;\n      const sweepFront = phaseProgress * (maxDiag + 1);\n\n      for (let pc = 0; pc < pixelCols; pc++) {\n        for (let row = 0; row < height; row++) {\n          const diag = pc + row;\n          const show = clearPhase ? diag >= sweepFront : diag < sweepFront;\n          if (show) {\n            const charIdx = Math.floor(pc / 2);\n            const dc = pc % 2;\n            field[charIdx] = setDot(field[charIdx], row, dc);\n          }\n        }\n      }\n      return field;\n    },\n  },\n\n  scan: {\n    totalFrames: 60,\n    interval: 40,\n    gridSize: [4, 4],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const period = 900;\n      const t = frame * 40;\n      const progress = (t / period) % 1;\n      const field = createFieldBuffer(width);\n      const scanX = progress * (width * 2 - 1);\n      const sigma = 0.45;\n\n      for (let pc = 0; pc < width * 2; pc++) {\n        const dist = Math.abs(pc - scanX);\n        const alpha = Math.exp(-(dist * dist) / (2 * sigma * sigma));\n        if (alpha > 0.1) {\n          const charIdx = Math.floor(pc / 2);\n          const dc = pc % 2;\n          for (let row = 0; row < height; row++) {\n            field[charIdx] = setDot(field[charIdx], row, dc);\n          }\n        }\n      }\n      return field;\n    },\n  },\n\n  fillSweep: {\n    totalFrames: 80,\n    interval: 60,\n    gridSize: [4, 4],\n\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const field = createFieldBuffer(width);\n\n      const framesPerStep = 2;\n\n      const rawStep = frame / framesPerStep;\n      const baseStep = Math.floor(rawStep);\n      const phase = rawStep - baseStep;\n\n      const maxFill = height;\n      const cycle = maxFill * 2;\n\n      const triangle = (s: number) => maxFill - Math.abs((s % cycle) - maxFill);\n\n      const levelA = triangle(baseStep);\n      const levelB = triangle(baseStep + 1);\n\n      // ✅ temporal smoothing\n      const fillLevel = phase < 0.5 ? levelA : levelB;\n\n      const maxRow = height - 1;\n\n      for (let i = 0; i < fillLevel; i++) {\n        const row = maxRow - i;\n\n        for (let charIdx = 0; charIdx < width; charIdx++) {\n          field[charIdx] = setDot(field[charIdx], row, 0);\n          field[charIdx] = setDot(field[charIdx], row, 1);\n        }\n      }\n\n      return field;\n    },\n  },\n  helix: {\n    totalFrames: 64,\n    interval: 40,\n    gridSize: [5, 5],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const field = createFieldBuffer(width);\n      const pixelCols = width * 2;\n      const drawableHeight = Math.min(height, 4);\n      if (drawableHeight < 2) return field;\n      const maxRow = drawableHeight - 1;\n\n      const progress = (frame % totalFrames) / Math.max(1, totalFrames - 1);\n      const ramp = smoothstep(progress);\n      const speedFactor = 0.7 + ramp * 0.7;\n      const scrollSteps = progress * (pixelCols + 8) * speedFactor;\n      const shiftA = Math.floor(scrollSteps);\n      const shiftB = shiftA + 1;\n      const shiftBlend = scrollSteps - shiftA;\n\n      const levelScale = maxRow / 3;\n      const mapLevel = (level: number) => clamp(Math.round(level * levelScale), 0, maxRow);\n\n      const chainStates: Array<{ a: number; b: number; bridge: boolean }> = [\n        { a: 0, b: 3, bridge: false },\n        { a: 1, b: 2, bridge: true },\n        { a: 2, b: 1, bridge: true },\n        { a: 3, b: 0, bridge: false },\n      ];\n\n      const drawDot = (pc: number, row: number) => {\n        if (pc < 0 || pc >= pixelCols || row < 0 || row >= drawableHeight) return;\n        const charIdx = Math.floor(pc / 2);\n        const dc = pc % 2;\n        field[charIdx] = setDot(field[charIdx], row, dc);\n      };\n\n      for (let pc = 0; pc < pixelCols; pc++) {\n        const idxA = (((pc + shiftA) % chainStates.length) + chainStates.length) % chainStates.length;\n        const idxB = (((pc + shiftB) % chainStates.length) + chainStates.length) % chainStates.length;\n\n        const columnPhase = (pc + 0.5) / pixelCols;\n        const useB = columnPhase < shiftBlend;\n        const state = useB ? chainStates[idxB] : chainStates[idxA];\n\n        const rowA = mapLevel(state.a);\n        const rowB = mapLevel(state.b);\n\n        drawDot(pc, rowA);\n        drawDot(pc, rowB);\n\n        if (state.bridge) {\n          const bridge = clamp(Math.round((rowA + rowB) / 2), 0, maxRow);\n          drawDot(pc, bridge);\n        }\n      }\n\n      return field;\n    },\n  },\n\n  braille: {\n    totalFrames: 60,\n    interval: 40,\n    gridSize: [4, 4],\n    compute: (frame: number, totalFrames: number, width: number, height: number, _ctx: PrecomputeContext) => {\n      const progress = frame / totalFrames;\n      const field = createFieldBuffer(width);\n\n      // Braille pattern positions within one character\n      const braillePath: Array<{ row: number; dc: number }> = [\n        { row: 0, dc: 0 }, // top-left\n        { row: 0, dc: 1 }, // top-right\n        { row: 1, dc: 1 }, // right-top\n        { row: 2, dc: 1 }, // right-bottom\n        { row: 3, dc: 1 }, // bottom-right\n        { row: 3, dc: 0 }, // bottom-left\n        { row: 2, dc: 0 }, // left-bottom\n        { row: 1, dc: 0 }, // left-top\n      ];\n\n      // 2 moving dots (opposite positions) with 2-dot trails each\n      const lead1Index = Math.floor(progress * braillePath.length) % braillePath.length;\n      const lead2Index = Math.floor((progress + 0.5) * braillePath.length) % braillePath.length;\n\n      // Only use first character\n      const charIdx = 0;\n\n      // First moving dot with 2-dot trail\n      for (let i = 0; i < 2; i++) {\n        const idx = (lead1Index - i + braillePath.length) % braillePath.length;\n        const pos = braillePath[idx];\n        if (pos.row < height) {\n          field[charIdx] = setDot(field[charIdx], pos.row, pos.dc);\n        }\n      }\n\n      // Second moving dot with 2-dot trail (opposite)\n      for (let i = 0; i < 2; i++) {\n        const idx = (lead2Index - i + braillePath.length) % braillePath.length;\n        const pos = braillePath[idx];\n        if (pos.row < height) {\n          field[charIdx] = setDot(field[charIdx], pos.row, pos.dc);\n        }\n      }\n\n      return field;\n    },\n  },\n\n  phaseShift: {\n    totalFrames: 60,\n    interval: 40,\n    gridSize: [5, 5],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const field = createFieldBuffer(width);\n      const progress = totalFrames > 1 ? frame / (totalFrames - 1) : 0;\n      const drawableHeight = Math.min(height, 4);\n      const centerX = (width * 2 - 1) / 2;\n      const centerY = (drawableHeight - 1) / 2;\n\n      const phasePosition = (progress * 8) % 4;\n\n      for (let pc = 0; pc < width * 2; pc++) {\n        for (let row = 0; row < drawableHeight; row++) {\n          const isLeft = pc < centerX;\n          const isTop = row < centerY;\n\n          let quadrantIndex = 0;\n          if (isTop && isLeft) quadrantIndex = 0;\n          else if (isTop && !isLeft) quadrantIndex = 1;\n          else if (!isTop && !isLeft) quadrantIndex = 2;\n          else quadrantIndex = 3;\n\n          const phaseDeltaRaw = Math.abs(phasePosition - quadrantIndex);\n          const phaseDelta = Math.min(phaseDeltaRaw, 4 - phaseDeltaRaw);\n\n          const primary = Math.max(0, 1 - phaseDelta / 0.7);\n          const secondary = Math.max(0, 1 - phaseDelta / 1.35) * 0.45;\n          const intensity = primary + secondary;\n\n          if (intensity > 0.42) {\n            const charIdx = Math.floor(pc / 2);\n            const dc = pc % 2;\n            field[charIdx] = setDot(field[charIdx], row, dc);\n          }\n        }\n      }\n      return field;\n    },\n  },\n\n  reflectedRipple: {\n    totalFrames: 60,\n    interval: 40,\n    gridSize: [6, 6],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const field = createFieldBuffer(width);\n      const progress = totalFrames > 1 ? frame / (totalFrames - 1) : 0;\n      const drawableHeight = Math.min(height, 4);\n      const centerX = (width * 2 - 1) / 2;\n      const outward = progress < 0.5;\n      const local = outward ? progress / 0.5 : (progress - 0.5) / 0.5;\n      const localClamped = clamp(local, 0, 1);\n      const easedLocal = smoothstep(localClamped) * 0.2 + localClamped * 0.8;\n      const radius = outward ? easedLocal * centerX : (1 - easedLocal) * centerX;\n      const ringThickness = 1.1;\n\n      for (let pc = 0; pc < width * 2; pc++) {\n        const distFromCenter = Math.abs(pc - centerX);\n        const bandStrength = Math.max(0, 1 - Math.abs(distFromCenter - radius) / ringThickness);\n\n        if (bandStrength > 0.3) {\n          const charIdx = Math.floor(pc / 2);\n          const dc = pc % 2;\n\n          for (let row = 0; row < drawableHeight; row++) {\n            field[charIdx] = setDot(field[charIdx], row, dc);\n          }\n        }\n      }\n\n      return field;\n    },\n  },\n\n  equalizer: {\n    totalFrames: 90,\n    interval: 40,\n    gridSize: [5, 4],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const field = createFieldBuffer(width);\n      const progress = frame / totalFrames;\n      const pixelCols = width * 2;\n      const t = progress * Math.PI * 2;\n\n      for (let pc = 0; pc < pixelCols; pc++) {\n        const seed = pc * 1.618033988749895;\n        const freq1 = 3 + (seed % 3);\n        const freq2 = 5 + ((seed * 1.3) % 4);\n        const phase1 = seed * 0.7;\n        const phase2 = seed * 1.3;\n        const amp1 = 0.6 + ((seed * 0.17) % 0.4);\n        const amp2 = 0.4 + ((seed * 0.23) % 0.3);\n\n        const wave1 = Math.sin(t * freq1 + phase1) * amp1;\n        const wave2 = Math.sin(t * freq2 + phase2) * amp2;\n        const wave3 = Math.sin(t * 7 + seed) * 0.2;\n\n        const combined = wave1 + wave2 + wave3;\n        const normalized = (combined + amp1 + amp2 + 0.2) / (2 * (amp1 + amp2 + 0.2));\n        const fillHeight = Math.max(0, Math.min(height, Math.floor(normalized * height)));\n\n        const charIdx = Math.floor(pc / 2);\n        const dc = pc % 2;\n\n        for (let row = height - 1; row >= height - fillHeight; row--) {\n          if (row >= 0 && row < height) {\n            field[charIdx] = setDot(field[charIdx], row, dc);\n          }\n        }\n      }\n\n      return field;\n    },\n  },\n\n  chase: {\n    totalFrames: 48,\n    interval: 60,\n    gridSize: [5, 4],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const field = createFieldBuffer(width);\n      const progress = frame / totalFrames;\n      const pixelCols = width * 2;\n      const drawableHeight = Math.min(height, 4);\n      const row = Math.floor((drawableHeight - 1) / 2);\n      const head = Math.floor(progress * pixelCols) % pixelCols;\n\n      for (let i = 0; i < 4; i++) {\n        const pc = (head - i + pixelCols) % pixelCols;\n        const charIdx = Math.floor(pc / 2);\n        const dc = pc % 2;\n        field[charIdx] = setDot(field[charIdx], row, dc);\n\n        if (i === 0) {\n          field[charIdx] = setDot(field[charIdx], Math.max(0, row - 1), dc);\n          field[charIdx] = setDot(field[charIdx], Math.min(drawableHeight - 1, row + 1), dc);\n        }\n      }\n\n      return field;\n    },\n  },\n\n  bars: {\n    totalFrames: 64,\n    interval: 50,\n    gridSize: [5, 4],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const field = createFieldBuffer(width);\n      const progress = frame / totalFrames;\n      const pixelCols = width * 2;\n      const drawableHeight = Math.min(height, 4);\n      const phase = progress * Math.PI * 2;\n\n      for (let pc = 0; pc < pixelCols; pc++) {\n        const distanceFromCenter = Math.abs(pc - (pixelCols - 1) / 2) / Math.max(1, pixelCols / 2);\n        const wave = (Math.sin(phase - distanceFromCenter * Math.PI * 1.7) + 1) / 2;\n        const barHeight = clamp(Math.round(1 + wave * (drawableHeight - 1)), 1, drawableHeight);\n        const charIdx = Math.floor(pc / 2);\n        const dc = pc % 2;\n\n        for (let i = 0; i < barHeight; i++) {\n          const row = drawableHeight - 1 - i;\n          field[charIdx] = setDot(field[charIdx], row, dc);\n        }\n      }\n\n      return field;\n    },\n  },\n\n  marquee: {\n    totalFrames: 48,\n    interval: 55,\n    gridSize: [5, 4],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const field = createFieldBuffer(width);\n      const pixelCols = width * 2;\n      const drawableHeight = Math.min(height, 4);\n      const offset = Math.floor((frame / totalFrames) * 8);\n\n      for (let pc = 0; pc < pixelCols; pc++) {\n        for (let row = 0; row < drawableHeight; row++) {\n          const stripe = (pc + row + offset) % 4;\n          if (stripe < 2) {\n            const charIdx = Math.floor(pc / 2);\n            field[charIdx] = setDot(field[charIdx], row, pc % 2);\n          }\n        }\n      }\n\n      return field;\n    },\n  },\n\n  typing: {\n    totalFrames: 80,\n    interval: 50,\n    gridSize: [4, 3],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const field = createFieldBuffer(width);\n      const totalCells = width * height;\n\n      const cycleLength = totalCells + 6;\n      const currentCell = Math.floor((frame / totalFrames) * cycleLength * 2) % cycleLength;\n\n      for (let cell = 0; cell < currentCell && cell < totalCells; cell++) {\n        const charIdx = cell % width;\n        const row = Math.floor(cell / width);\n        field[charIdx] = setDot(field[charIdx], row, 0);\n        field[charIdx] = setDot(field[charIdx], row, 1);\n      }\n\n      const cursorPos = Math.min(currentCell, totalCells - 1);\n      const blinkOn = Math.floor(frame / 1) % 2 === 0;\n\n      if (blinkOn && cursorPos >= 0) {\n        const cursorCharIdx = cursorPos % width;\n        const cursorRow = Math.floor(cursorPos / width);\n        field[cursorCharIdx] = setDot(field[cursorCharIdx], cursorRow, 0);\n        field[cursorCharIdx] = setDot(field[cursorCharIdx], cursorRow, 1);\n      }\n\n      return field;\n    },\n  },\n\n  spiral: {\n    totalFrames: 60,\n    interval: 40,\n    gridSize: [5, 5],\n    compute: (frame, totalFrames, width, height, _ctx) => {\n      const field = createFieldBuffer(width);\n      const progress = frame / totalFrames;\n      const pixelCols = width * 2;\n      const drawableHeight = Math.min(height, 4);\n      const centerX = (pixelCols - 1) / 2;\n      const centerY = (drawableHeight - 1) / 2;\n\n      const arms = 3;\n      const armLength = Math.max(pixelCols, drawableHeight) / 2;\n      const rotationOffset = progress * Math.PI * 4;\n\n      for (let arm = 0; arm < arms; arm++) {\n        const armAngle = (arm / arms) * Math.PI * 2 + rotationOffset;\n\n        for (let r = 0; r < armLength; r++) {\n          const t = r / armLength;\n          const spiralAngle = armAngle + t * Math.PI * 1.5;\n          const x = centerX + Math.cos(spiralAngle) * r * 0.8;\n          const y = centerY + Math.sin(spiralAngle) * r * 0.8;\n\n          const pc = Math.round(x);\n          const row = Math.round(y);\n\n          if (pc >= 0 && pc < pixelCols && row >= 0 && row < drawableHeight) {\n            const charIdx = Math.floor(pc / 2);\n            const dc = pc % 2;\n            field[charIdx] = setDot(field[charIdx], row, dc);\n          }\n        }\n      }\n\n      return field;\n    },\n  },\n};\n\nfunction toCamelCase(str: string): string {\n  return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());\n}\n\nconst frameCache = new Map<string, string[]>();\n\nexport function generateFrames(variant: string, width: number, height: number): { frames: string[]; interval: number } {\n  const key = `${variant}-${width}x${height}`;\n  const cached = frameCache.get(key);\n  if (cached) {\n    return { frames: cached, interval: VARIANT_CONFIGS[toCamelCase(variant)]?.interval || 40 };\n  }\n\n  const config = VARIANT_CONFIGS[toCamelCase(variant)];\n  if (!config) {\n    return { frames: [fieldToString(createFieldBuffer(width))], interval: 40 };\n  }\n\n  const defaultArea = config.gridSize[0] * config.gridSize[1];\n  const customArea = width * height;\n  const scaleFactor = customArea / defaultArea;\n  const clampedScale = Math.max(0.5, Math.min(3, scaleFactor));\n  const scaledFrames = Math.round(config.totalFrames * clampedScale);\n  const totalFrames = Math.max(30, scaledFrames);\n\n  const context = getPrecomputeContext(width, height);\n  const frames: string[] = [];\n\n  for (let frame = 0; frame < totalFrames; frame++) {\n    const field = config.compute(frame, totalFrames, width, height, context);\n    frames.push(fieldToString(field));\n  }\n\n  frameCache.set(key, frames);\n  return { frames, interval: config.interval };\n}\n\nexport function getVariantGridSize(variant: string): [number, number] {\n  const config = VARIANT_CONFIGS[toCamelCase(variant)];\n  if (config?.gridSize) {\n    return config.gridSize;\n  }\n  return [4, 4];\n}\n\nexport function normalizeVariant(variant?: string): BrailleLoaderVariant {\n  if (!variant) return \"breathe\";\n  return brailleLoaderVariants.includes(variant as BrailleLoaderVariant) ? (variant as BrailleLoaderVariant) : \"breathe\";\n}\n",
      "type": "registry:lib",
      "target": "@lib/braille-loader.ts"
    }
  ]
}