{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "TechText-JS-CSS",
	"title": "TechText",
	"description": "A wordmark whose letters turn into dashed vector paths under the cursor.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "TechText.css",
			"target": "@components/TechText.css",
			"content": ".tech-text {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  touch-action: pan-y;\n  user-select: none;\n}\n\n.tech-text-canvas {\n  display: block;\n  width: 100%;\n  height: 100%;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "TechText.jsx",
			"content": "'use client';\n\nimport { useEffect, useRef } from 'react';\n\nimport './TechText.css';\n\nconst LABEL_FONT = '10px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace';\nconst FALLOFF_STEPS = 8;\nconst SPRING = 320;\nconst INHERIT = /^(currentcolor|inherit)$/i;\nconst INTRO_DELAY = 0.15;\nconst KICK_DECAY = 0.62;\n\nconst approach = (current, target, dt, seconds) => current + (target - current) * (1 - Math.exp(-dt / seconds));\n\nconst clamp01 = value => Math.min(1, Math.max(0, Number(value) || 0));\n\nconst smoothstep = (edge0, edge1, value) => {\n  const t = clamp01((value - edge0) / (edge1 - edge0));\n  return t * t * (3 - 2 * t);\n};\n\nconst noise = (...values) => {\n  let h = 2166136261;\n  for (const value of values) {\n    h = Math.imul(h ^ (value | 0), 16777619);\n    h ^= h >>> 13;\n    h = Math.imul(h, 0x5bd1e995);\n    h ^= h >>> 15;\n  }\n  return (h >>> 0) / 4294967296;\n};\n\nconst signed = value => (value > 0 ? `+${value}` : value < 0 ? `−${-value}` : '0');\n\nconst TechText = ({\n  text = 'React Bits',\n  fontFamily = '',\n  fontWeight = 600,\n  fontSize = 150,\n  letterSpacing = -0.05,\n  color = 'currentColor',\n  accentColor = 'currentColor',\n  align = 'center',\n  reach = 200,\n  softness = 0.7,\n  dashLength = 4,\n  dashGap = 2,\n  strokeWidth = 1.5,\n  lineStyle = 'dashed',\n  reveal = 'letter',\n  specks = 15,\n  selection = true,\n  labels = true,\n  draggable = true,\n  bounce = 0.4,\n  sweep = true,\n  speed = 1,\n  clickKick = true,\n  intro = 'none',\n  introDuration = 2,\n  guides = false,\n  className = '',\n  style\n}) => {\n  const containerRef = useRef(null);\n  const canvasRef = useRef(null);\n  const settingsRef = useRef(null);\n  const wakeRef = useRef(() => {});\n\n  useEffect(() => {\n    settingsRef.current = {\n      text,\n      fontFamily,\n      fontWeight,\n      fontSize,\n      letterSpacing,\n      color,\n      accentColor,\n      align,\n      reach,\n      softness,\n      dashLength,\n      dashGap,\n      strokeWidth,\n      lineStyle,\n      reveal,\n      specks,\n      selection,\n      labels,\n      draggable,\n      bounce,\n      sweep,\n      speed,\n      clickKick,\n      intro,\n      introDuration,\n      guides\n    };\n    wakeRef.current();\n  });\n\n  useEffect(() => {\n    const container = containerRef.current;\n    const canvas = canvasRef.current;\n    const ctx = canvas?.getContext('2d');\n    const scratch = document.createElement('canvas');\n    const scratchCtx = scratch.getContext('2d');\n    if (!container || !canvas || !ctx || !scratchCtx) return undefined;\n\n    const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches;\n    let width = 1;\n    let height = 1;\n    let dpr = 1;\n    let raf = 0;\n    let last = performance.now();\n    let visible = false;\n    let alive = true;\n    let layoutKey = '';\n    let sizeDirty = true;\n    let requestedFont = '';\n    let word = null;\n    let glyphs = [];\n    let presence = 0;\n    let clock = 0;\n    let pulse = 0;\n    let placed = false;\n    let dragging = -1;\n    let cursor = '';\n    let introClock = 0;\n    let introKind = '';\n    let introducing = false;\n    let fontsReady = !document.fonts;\n    let guideAlpha = 0;\n    const kicks = [];\n    const press = { x: 0, y: 0, time: 0, index: -1 };\n    const pointer = { x: 0, y: 0, inside: false };\n    const grab = { x: 0, y: 0 };\n    const lens = { x: 0, y: 0 };\n    const frame = { x1: 0, y1: 0, x2: 0, y2: 0, alpha: 0, index: -1 };\n\n    const refreshFonts = () => {\n      layoutKey = '';\n      wakeRef.current();\n    };\n\n    const fontsLoaded = () => {\n      fontsReady = true;\n      refreshFonts();\n    };\n\n    const family = s => s.fontFamily || getComputedStyle(container).fontFamily || 'sans-serif';\n    const fontFor = (s, size) => `${s.fontWeight} ${size}px ${family(s)}`;\n\n    const palette = s => {\n      const current = getComputedStyle(container).color || '#000000';\n      const pick = value => (!value || INHERIT.test(String(value).trim()) ? current : value);\n      return { ink: pick(s.color), accent: pick(s.accentColor) };\n    };\n\n    const setFont = (target, s, size) => {\n      target.font = fontFor(s, size);\n      if ('letterSpacing' in target) target.letterSpacing = `${s.letterSpacing * size}px`;\n      target.textAlign = 'left';\n      target.textBaseline = 'alphabetic';\n    };\n\n    const sprite = (s, view, glyph, stroke, ink) => {\n      const pad = Math.ceil(s.strokeWidth * 2 + 4);\n      const left = glyph.box.x1 - pad;\n      const top = glyph.box.y1 - pad;\n      const w = glyph.box.x2 - glyph.box.x1 + pad * 2;\n      const h = glyph.box.y2 - glyph.box.y1 + pad * 2;\n      const image = document.createElement('canvas');\n      image.width = Math.max(1, Math.ceil(w * dpr));\n      image.height = Math.max(1, Math.ceil(h * dpr));\n      const c = image.getContext('2d');\n      if (!c) return { image, left, top };\n      c.setTransform(dpr, 0, 0, dpr, -left * dpr, -top * dpr);\n      setFont(c, s, view.size);\n      if (stroke) {\n        c.lineJoin = 'round';\n        c.lineWidth = s.strokeWidth * 2;\n        c.lineCap = 'butt';\n        c.strokeStyle = ink;\n        if (s.lineStyle !== 'solid') c.setLineDash([Math.max(1, s.dashLength), Math.max(1, s.dashGap)]);\n        c.strokeText(glyph.char, glyph.x, view.baseline);\n        c.setLineDash([]);\n        c.globalCompositeOperation = 'destination-out';\n        c.fillStyle = '#000000';\n        c.fillText(glyph.char, glyph.x, view.baseline);\n        c.globalCompositeOperation = 'source-over';\n      } else {\n        c.fillStyle = ink;\n        c.fillText(glyph.char, glyph.x, view.baseline);\n      }\n      return { image, left, top };\n    };\n\n    const ensureLayout = (s, ink) => {\n      const key = [\n        s.text,\n        family(s),\n        s.fontWeight,\n        s.fontSize,\n        s.letterSpacing,\n        s.align,\n        ink,\n        s.dashLength,\n        s.dashGap,\n        s.strokeWidth,\n        s.lineStyle,\n        width,\n        height,\n        dpr\n      ].join('|');\n      if (key === layoutKey && word) return word;\n      layoutKey = key;\n      const wanted = fontFor(s, 64);\n      if (document.fonts && wanted !== requestedFont) {\n        requestedFont = wanted;\n        document.fonts.load(wanted, s.text).then(refreshFonts, refreshFonts);\n      }\n\n      const probe = scratchCtx;\n      setFont(probe, s, s.fontSize);\n      let m = probe.measureText(s.text);\n      const fit = Math.min(\n        1,\n        (width * 0.9) / Math.max(m.actualBoundingBoxLeft + m.actualBoundingBoxRight, 1),\n        (height * 0.66) / Math.max(m.actualBoundingBoxAscent + m.actualBoundingBoxDescent, 1)\n      );\n      const size = s.fontSize * fit;\n      setFont(probe, s, size);\n      m = probe.measureText(s.text);\n      const inkWidth = m.actualBoundingBoxLeft + m.actualBoundingBoxRight;\n      const inkHeight = m.actualBoundingBoxAscent + m.actualBoundingBoxDescent;\n      const margin = width * 0.05;\n      const start =\n        s.align === 'left' ? margin : s.align === 'right' ? width - margin - inkWidth : (width - inkWidth) / 2;\n      const x = start + m.actualBoundingBoxLeft;\n      const baseline = (height - inkHeight) / 2 + m.actualBoundingBoxAscent;\n      const xHeight = baseline - probe.measureText('x').actualBoundingBoxAscent;\n      const capHeight = baseline - probe.measureText('H').actualBoundingBoxAscent;\n      const ascender = baseline - probe.measureText('d').actualBoundingBoxAscent;\n      const descender = baseline + probe.measureText('p').actualBoundingBoxDescent;\n      const top = baseline - m.actualBoundingBoxAscent;\n      const bottom = baseline + m.actualBoundingBoxDescent;\n      const inkTop = Math.min(top, capHeight) - 2;\n      const inkBottom = Math.max(bottom, baseline) + 2;\n      const next = {\n        size,\n        baseline,\n        left: x - m.actualBoundingBoxLeft,\n        right: x + m.actualBoundingBoxRight,\n        top,\n        bottom,\n        lines: [\n          { y: baseline, label: 'baseline' },\n          { y: xHeight, label: 'x-height' },\n          { y: capHeight, label: 'cap height' },\n          ...(capHeight - ascender > Math.max(11, size * 0.06) ? [{ y: ascender, label: 'ascender' }] : []),\n          { y: descender, label: 'descender' }\n        ],\n        bands: [\n          { y0: xHeight, y1: baseline, up: true, order: 0 },\n          { y0: capHeight, y1: xHeight, up: true, order: 1 },\n          { y0: inkTop, y1: capHeight, up: true, order: 2 },\n          { y0: baseline, y1: inkBottom, up: false, order: 1 }\n        ].filter(band => band.y1 - band.y0 > 0.5)\n      };\n      word = next;\n\n      const chars = Array.from(s.text);\n      const previous = glyphs;\n      glyphs = [];\n      let prefix = '';\n      chars.forEach((char, i) => {\n        prefix += char;\n        const own = probe.measureText(char);\n        const gx = x + probe.measureText(prefix).width - own.width;\n        if (!char.trim()) return;\n        const base = {\n          char,\n          x: gx,\n          box: {\n            x1: gx - own.actualBoundingBoxLeft,\n            y1: baseline - own.actualBoundingBoxAscent,\n            x2: gx + own.actualBoundingBoxRight,\n            y2: baseline + own.actualBoundingBoxDescent\n          }\n        };\n        const kept = previous[glyphs.length];\n        const sameGlyph = kept?.char === char && kept.index === i;\n        glyphs.push({\n          ...base,\n          offset: sameGlyph ? kept.offset : { x: 0, y: 0 },\n          velocity: sameGlyph ? kept.velocity : { x: 0, y: 0 },\n          outline: sameGlyph ? kept.outline : 0,\n          shown: sameGlyph ? kept.shown : 1,\n          cells: null,\n          assembled: 0,\n          index: i,\n          fill: sprite(s, next, base, false, ink),\n          dashes: sprite(s, next, base, true, ink)\n        });\n      });\n      if (dragging >= 0 && glyphs[dragging]?.index !== previous[dragging]?.index) dragging = -1;\n      if (frame.index >= 0 && glyphs[frame.index]?.char !== previous[frame.index]?.char) frame.index = -1;\n      return next;\n    };\n\n    const glyphAt = (x, y) => {\n      if (!word || y < word.top - 24 || y > word.bottom + 24) return -1;\n      let best = -1;\n      let bestDistance = Infinity;\n      glyphs.forEach((glyph, i) => {\n        const x1 = glyph.box.x1 + glyph.offset.x;\n        const x2 = glyph.box.x2 + glyph.offset.x;\n        const d = x < x1 ? x1 - x : x > x2 ? x - x2 : 0;\n        if (d < bestDistance) {\n          bestDistance = d;\n          best = i;\n        }\n      });\n      return bestDistance < 28 ? best : -1;\n    };\n\n    const falloff = (target, cx, cy, radius, strength, softness) => {\n      const inner = Math.min(1, Math.max(0, 1 - softness));\n      const gradient = target.createRadialGradient(cx, cy, 0, cx, cy, radius);\n      gradient.addColorStop(0, `rgba(0, 0, 0, ${strength})`);\n      if (inner > 0.995) {\n        gradient.addColorStop(0.995, `rgba(0, 0, 0, ${strength})`);\n        gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');\n        return gradient;\n      }\n      for (let i = 0; i <= FALLOFF_STEPS; i++) {\n        const t = i / FALLOFF_STEPS;\n        const eased = t * t * (3 - 2 * t);\n        gradient.addColorStop(inner + (1 - inner) * t, `rgba(0, 0, 0, ${strength * (1 - eased)})`);\n      }\n      return gradient;\n    };\n\n    const blit = (target, art, dx, dy, originX, originY) => {\n      target.drawImage(\n        art.image,\n        Math.round((art.left + dx) * dpr - originX),\n        Math.round((art.top + dy) * dpr - originY)\n      );\n    };\n\n    const drawReveal = s => {\n      const radius = s.reach * dpr;\n      const cx = lens.x * dpr;\n      const cy = lens.y * dpr;\n      ctx.globalCompositeOperation = 'destination-out';\n      ctx.fillStyle = falloff(ctx, cx, cy, radius, presence, s.softness);\n      ctx.fillRect(cx - radius, cy - radius, radius * 2, radius * 2);\n      ctx.globalCompositeOperation = 'source-over';\n\n      const x0 = Math.max(0, Math.floor(cx - radius));\n      const y0 = Math.max(0, Math.floor(cy - radius));\n      const x1 = Math.min(canvas.width, Math.ceil(cx + radius));\n      const y1 = Math.min(canvas.height, Math.ceil(cy + radius));\n      if (x1 <= x0 || y1 <= y0) return;\n      const w = x1 - x0;\n      const h = y1 - y0;\n      if (scratch.width < w || scratch.height < h) {\n        scratch.width = Math.max(scratch.width, w);\n        scratch.height = Math.max(scratch.height, h);\n      }\n      scratchCtx.setTransform(1, 0, 0, 1, 0, 0);\n      scratchCtx.globalCompositeOperation = 'source-over';\n      scratchCtx.clearRect(0, 0, w, h);\n      for (const glyph of glyphs) blit(scratchCtx, glyph.dashes, glyph.offset.x, glyph.offset.y, x0, y0);\n      scratchCtx.globalCompositeOperation = 'destination-in';\n      scratchCtx.fillStyle = falloff(scratchCtx, cx - x0, cy - y0, radius, 1, s.softness);\n      scratchCtx.fillRect(0, 0, w, h);\n      scratchCtx.globalCompositeOperation = 'source-over';\n      ctx.globalAlpha = presence;\n      ctx.drawImage(scratch, 0, 0, w, h, x0, y0, w, h);\n      ctx.globalAlpha = 1;\n    };\n\n    const crisp = value => (Math.round(value * dpr) + 0.5) / dpr;\n\n    const perimeterPoint = (distance, w, h) => {\n      let d = ((distance % (2 * (w + h))) + 2 * (w + h)) % (2 * (w + h));\n      if (d < w) return [frame.x1 + d, frame.y1, 0, -1];\n      d -= w;\n      if (d < h) return [frame.x2, frame.y1 + d, 1, 0];\n      d -= h;\n      if (d < w) return [frame.x2 - d, frame.y2, 0, 1];\n      d -= w;\n      return [frame.x1, frame.y2 - d, -1, 0];\n    };\n\n    const drawSpecks = (s, a, accent) => {\n      const w = frame.x2 - frame.x1;\n      const h = frame.y2 - frame.y1;\n      if (w < 2 || h < 2) return;\n      const perimeter = 2 * (w + h);\n      const seed = frame.index + 1;\n      const grid = 3;\n      ctx.fillStyle = accent;\n      ctx.strokeStyle = accent;\n\n      for (let k = 0; k < s.specks; k++) {\n        const period = 0.5 + noise(seed, k, 11) * 1.2;\n        const t = pulse / period + noise(seed, k, 17);\n        const cycle = Math.floor(t);\n        const life = t - cycle;\n        if (life > 0.7) continue;\n        const [px, py, nx, ny] = perimeterPoint(noise(seed, k, cycle) * perimeter, w, h);\n        const pick = noise(seed, k, cycle, 2);\n        const size = pick < 0.46 ? 2 : pick < 0.7 ? 3 : pick < 0.84 ? 5 : pick < 0.94 ? 8 : 11;\n        const large = size >= 8;\n        const out = (large ? 9 : 4) + Math.floor(noise(seed, k, cycle, 1) * 5) * grid;\n        const x = frame.x1 + Math.round((px + nx * out - frame.x1) / grid) * grid;\n        const y = frame.y1 + Math.round((py + ny * out - frame.y1) / grid) * grid;\n        const tone = noise(seed, k, cycle, 3);\n        const blink = life < 0.06 || (life > 0.32 && life < 0.36) ? 0.35 : 1;\n        const alpha = a * (large ? 0.3 + 0.4 * tone : 0.3 + 0.6 * tone) * blink;\n        const left = Math.round(x - size / 2);\n        const top = Math.round(y - size / 2);\n        ctx.globalAlpha = alpha;\n        if (tone < 0.26 || (large && tone < 0.78)) {\n          ctx.strokeRect(left + 0.5, top + 0.5, size, size);\n          if (large && tone > 0.5) ctx.fillRect(Math.round(x) - 1, Math.round(y) - 1, 2, 2);\n        } else {\n          ctx.fillRect(left, top, size, size);\n        }\n      }\n\n      for (let j = 0; j < 2; j++) {\n        const head = (pulse * 0.42 * s.speed + j * 0.5) * perimeter;\n        for (let i = 0; i < 4; i++) {\n          const [x, y] = perimeterPoint(head - i * 6, w, h);\n          const size = i === 0 ? 3 : 2;\n          ctx.globalAlpha = a * [0.95, 0.55, 0.32, 0.16][i];\n          ctx.fillRect(Math.round(x - size / 2), Math.round(y - size / 2), size, size);\n        }\n      }\n      ctx.globalAlpha = 1;\n    };\n\n    const drawFrame = (s, accent) => {\n      const glyph = glyphs[frame.index];\n      if (!glyph || frame.alpha < 0.01) return;\n      const a = frame.alpha;\n      const x1 = crisp(frame.x1);\n      const y1 = crisp(frame.y1);\n      const x2 = crisp(frame.x2);\n      const y2 = crisp(frame.y2);\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ctx.fillStyle = accent;\n      ctx.strokeStyle = accent;\n      ctx.lineWidth = 1;\n\n      const moved = Math.hypot(glyph.offset.x, glyph.offset.y);\n      if (moved > 1) {\n        const hx = (glyph.box.x1 + glyph.box.x2) / 2;\n        const hy = (glyph.box.y1 + glyph.box.y2) / 2;\n        ctx.beginPath();\n        ctx.moveTo(hx, hy);\n        ctx.lineTo(hx + glyph.offset.x, hy + glyph.offset.y);\n        ctx.setLineDash([3, 4]);\n        ctx.globalAlpha = 0.45 * a;\n        ctx.stroke();\n        ctx.setLineDash([]);\n        ctx.beginPath();\n        ctx.rect(Math.round(hx) - 2, Math.round(hy) - 2, 4, 4);\n        ctx.globalAlpha = 0.7 * a;\n        ctx.fill();\n      }\n\n      ctx.beginPath();\n      ctx.rect(x1, y1, x2 - x1, y2 - y1);\n      ctx.globalAlpha = 0.5 * a;\n      ctx.stroke();\n\n      ctx.beginPath();\n      for (const [cx, cy] of [\n        [x1, y1],\n        [x2, y1],\n        [x2, y2],\n        [x1, y2]\n      ]) {\n        ctx.rect(Math.round(cx) - 2, Math.round(cy) - 2, 5, 5);\n      }\n      ctx.globalAlpha = 0.95 * a;\n      ctx.fill();\n      ctx.globalAlpha = 1;\n\n      if (s.specks > 0) drawSpecks(s, a, accent);\n\n      if (!s.labels) return;\n      ctx.font = LABEL_FONT;\n      ctx.textAlign = 'left';\n      ctx.textBaseline = 'bottom';\n      ctx.fillStyle = accent;\n      ctx.globalAlpha = 0.62 * a;\n      const label =\n        moved > 1\n          ? `${signed(Math.round(glyph.offset.x))}, ${signed(Math.round(-glyph.offset.y))}`\n          : `${glyph.char}  ${Math.round(glyph.box.x2 - glyph.box.x1)} × ${Math.round(glyph.box.y2 - glyph.box.y1)}`;\n      ctx.fillText(label, Math.round(frame.x1), Math.round(frame.y1) - 7);\n      ctx.globalAlpha = 1;\n    };\n\n    const drawLines = (s, view, accent, alpha, growth) => {\n      const pad = Math.max(12, view.size * 0.12);\n      const x1 = Math.round(view.left - pad);\n      const x2 = Math.round(view.right + pad);\n      const room = width - x2 > x1;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ctx.strokeStyle = accent;\n      ctx.fillStyle = accent;\n      ctx.lineWidth = 1;\n      ctx.font = LABEL_FONT;\n      ctx.textBaseline = 'middle';\n      ctx.textAlign = room ? 'left' : 'right';\n      view.lines.forEach((line, k) => {\n        const grow = growth(k);\n        if (grow <= 0.001) return;\n        const y = crisp(line.y);\n        const end = x1 + (x2 - x1) * grow;\n        ctx.globalAlpha = alpha * 0.4;\n        ctx.beginPath();\n        ctx.moveTo(x1, y);\n        ctx.lineTo(end, y);\n        ctx.stroke();\n        ctx.globalAlpha = alpha * 0.9;\n        ctx.fillRect(x1 - 1, Math.round(line.y) - 1, 3, 3);\n        ctx.fillRect(Math.round(end) - 1, Math.round(line.y) - 1, 3, 3);\n        if (s.labels) {\n          ctx.globalAlpha = alpha * 0.62 * smoothstep(0.7, 1, grow);\n          ctx.fillText(line.label, room ? x2 + 8 : x1 - 8, Math.round(line.y));\n        }\n      });\n      ctx.textAlign = 'left';\n      ctx.setTransform(1, 0, 0, 1, 0, 0);\n      ctx.globalAlpha = 1;\n    };\n\n    const ensureCells = (glyph, view) => {\n      if (glyph.cells) return glyph.cells;\n      const art = glyph.fill;\n      const size = Math.max(3, Math.round(view.size / 26));\n      const w = art.image.width;\n      const h = art.image.height;\n      const source = art.image.getContext('2d');\n      const cells = [];\n      glyph.cells = cells;\n      glyph.assembled = 0;\n      if (!source || w < 2 || h < 2) return cells;\n      const data = source.getImageData(0, 0, w, h).data;\n      const gx = Math.floor(art.left / size) * size;\n      const gy = Math.floor(art.top / size) * size;\n      const cols = Math.ceil((art.left + w / dpr - gx) / size) + 1;\n      const rows = Math.ceil((art.top + h / dpr - gy) / size) + 1;\n      const hit = new Uint8Array(cols * rows);\n      for (let py = 0; py < h; py++) {\n        for (let px = 0; px < w; px++) {\n          if (data[(py * w + px) * 4 + 3] < 24) continue;\n          const cx = Math.floor((art.left + px / dpr - gx) / size);\n          const cy = Math.floor((art.top + py / dpr - gy) / size);\n          hit[cy * cols + cx] = 1;\n        }\n      }\n      const center = (glyph.box.x1 + glyph.box.x2) / 2;\n      const along = clamp01((center - view.left) / Math.max(1, view.right - view.left));\n      for (let k = 0; k < hit.length; k++) {\n        if (!hit[k]) continue;\n        const angle = noise(glyph.index, k, 1) * Math.PI * 2;\n        const distance = view.size * (0.25 + 0.6 * noise(glyph.index, k, 2));\n        const start = 0.03 + 0.36 * along + 0.12 * noise(glyph.index, k, 3);\n        const land = start + 0.3 + 0.14 * noise(glyph.index, k, 4);\n        cells.push({\n          x: gx + (k % cols) * size,\n          y: gy + Math.floor(k / cols) * size,\n          size,\n          dx: Math.cos(angle) * distance,\n          dy: Math.sin(angle) * distance * 0.7,\n          start,\n          land\n        });\n        glyph.assembled = Math.max(glyph.assembled, land);\n      }\n      return cells;\n    };\n\n    const cellRect = (x, y, size) => {\n      const left = Math.round(x * dpr);\n      const top = Math.round(y * dpr);\n      return [left, top, Math.round((x + size) * dpr) - left, Math.round((y + size) * dpr) - top];\n    };\n\n    const drawAssembling = (glyph, progress, accent) => {\n      const cells = glyph.cells || [];\n      ctx.save();\n      ctx.beginPath();\n      let landed = 0;\n      for (const cell of cells) {\n        if (progress < cell.land) continue;\n        const [x, y, w, h] = cellRect(cell.x, cell.y, cell.size);\n        ctx.rect(x, y, w, h);\n        landed++;\n      }\n      if (landed) {\n        ctx.clip();\n        blit(ctx, glyph.fill, 0, 0, 0, 0);\n      }\n      ctx.restore();\n      ctx.fillStyle = accent;\n      for (const cell of cells) {\n        if (progress < cell.start || progress >= cell.land) continue;\n        const u = (progress - cell.start) / (cell.land - cell.start);\n        const eased = 1 - Math.pow(1 - u, 4);\n        const size = cell.size * (0.4 + 0.6 * eased);\n        const x = cell.x + (cell.size - size) / 2 + cell.dx * (1 - eased);\n        const y = cell.y + (cell.size - size) / 2 + cell.dy * (1 - eased);\n        const [left, top, w, h] = cellRect(x, y, size);\n        ctx.globalAlpha = Math.min(1, u * 4) * (0.55 + 0.45 * eased);\n        ctx.fillRect(left, top, Math.max(1, w), Math.max(1, h));\n      }\n      ctx.globalAlpha = 1;\n    };\n\n    const drawFilling = (glyph, lead, progress, view) => {\n      const art = glyph.fill;\n      const left = Math.floor(art.left * dpr);\n      const top = Math.floor(art.top * dpr);\n      const w = art.image.width + 2;\n      const h = art.image.height + 2;\n      const fill = clamp01((progress - 0.24 - 0.28 * lead) / 0.48);\n      const rects = [];\n      for (const band of view.bands) {\n        const amount = smoothstep(0.3 * band.order, 0.3 * band.order + 0.4, fill);\n        if (amount <= 0) continue;\n        const span = (band.y1 - band.y0) * amount;\n        const y0 = Math.round((band.up ? band.y1 - span : band.y0) * dpr);\n        const y1 = Math.round((band.up ? band.y1 : band.y0 + span) * dpr);\n        if (y1 > y0) rects.push([y0, y1 - y0]);\n      }\n      const outline = smoothstep(0.08 + 0.2 * lead, 0.22 + 0.2 * lead, progress);\n      if (outline > 0.001) {\n        ctx.save();\n        ctx.beginPath();\n        ctx.rect(left - 2, top - 2, w + 2, h + 2);\n        for (const [y, span] of rects) ctx.rect(left - 2, y, w + 2, span);\n        ctx.clip('evenodd');\n        ctx.globalAlpha = outline;\n        blit(ctx, glyph.dashes, 0, 0, 0, 0);\n        ctx.restore();\n      }\n      if (rects.length) {\n        ctx.save();\n        ctx.beginPath();\n        for (const [y, span] of rects) ctx.rect(left - 2, y, w + 2, span);\n        ctx.clip();\n        blit(ctx, glyph.fill, 0, 0, 0, 0);\n        ctx.restore();\n      }\n      ctx.globalAlpha = 1;\n    };\n\n    const kick = index => {\n      const strength = (word ? word.size : 120) * 11;\n      kicks.push({ index, at: pulse, velocity: strength });\n      for (let d = 1; d < glyphs.length; d++) {\n        const velocity = strength * Math.pow(KICK_DECAY, d);\n        if (velocity < 30) break;\n        for (const j of [index - d, index + d]) {\n          if (j >= 0 && j < glyphs.length) kicks.push({ index: j, at: pulse + d * 0.055, velocity });\n        }\n      }\n      wakeRef.current();\n    };\n\n    const tick = now => {\n      raf = 0;\n      const s = settingsRef.current;\n      if (!s) return;\n      if (sizeDirty) {\n        sizeDirty = false;\n        width = Math.max(1, container.clientWidth);\n        height = Math.max(1, container.clientHeight);\n        dpr = Math.min(window.devicePixelRatio || 1, 2);\n        const pixelWidth = Math.round(width * dpr);\n        const pixelHeight = Math.round(height * dpr);\n        if (canvas.width !== pixelWidth) canvas.width = pixelWidth;\n        if (canvas.height !== pixelHeight) canvas.height = pixelHeight;\n      }\n      const dt = Math.min(0.05, Math.max(0.001, (now - last) / 1000));\n      last = now;\n      const { ink, accent } = palette(s);\n      const view = ensureLayout(s, ink);\n      const damping = 2 * Math.sqrt(SPRING) * (1 - clamp01(s.bounce) * 0.96);\n\n      if (s.intro !== introKind) {\n        introKind = s.intro;\n        introClock = 0;\n      }\n      const introOn = introKind !== 'none' && !reducedMotion && glyphs.length > 0;\n      if (introOn && fontsReady) introClock += dt;\n      const progress = introOn ? clamp01((introClock - INTRO_DELAY) / Math.max(0.3, s.introDuration)) : 1;\n      introducing = introOn && progress < 1;\n\n      const sweeping = !introducing && s.sweep && !reducedMotion && !pointer.inside && dragging < 0;\n      if (sweeping) clock += dt * s.speed;\n      pulse += dt;\n      let targetX = pointer.x;\n      let targetY = pointer.y;\n      if (sweeping) {\n        targetX = view.left + (view.right - view.left) * (0.5 - 0.5 * Math.cos(clock * 0.45));\n        targetY = view.top + (view.bottom - view.top) * (0.45 + 0.1 * Math.sin(clock * 0.8));\n      }\n      const active = !introducing && (pointer.inside || sweeping || dragging >= 0);\n      if (active && !placed) {\n        lens.x = targetX;\n        lens.y = targetY;\n      }\n      if (active) {\n        const lag = pointer.inside ? 0.05 : 0.22;\n        lens.x = approach(lens.x, targetX, dt, lag);\n        lens.y = approach(lens.y, targetY, dt, lag);\n      }\n      placed = active;\n      presence = approach(presence, s.reveal === 'area' && active && dragging < 0 ? 1 : 0, dt, 0.16);\n\n      for (let k = kicks.length - 1; k >= 0; k--) {\n        if (kicks[k].at > pulse) continue;\n        const glyph = glyphs[kicks[k].index];\n        if (glyph) glyph.velocity.y -= kicks[k].velocity;\n        kicks.splice(k, 1);\n      }\n\n      let moving = false;\n      glyphs.forEach((glyph, i) => {\n        if (i === dragging) {\n          glyph.offset.x = approach(glyph.offset.x, pointer.x - grab.x, dt, 0.03);\n          glyph.offset.y = approach(glyph.offset.y, pointer.y - grab.y, dt, 0.03);\n          glyph.velocity.x = 0;\n          glyph.velocity.y = 0;\n          moving = true;\n          return;\n        }\n        const { offset, velocity } = glyph;\n        if (Math.abs(offset.x) < 0.05 && Math.abs(offset.y) < 0.05 && Math.hypot(velocity.x, velocity.y) < 0.5) {\n          offset.x = 0;\n          offset.y = 0;\n          velocity.x = 0;\n          velocity.y = 0;\n          return;\n        }\n        velocity.x += (-SPRING * offset.x - damping * velocity.x) * dt;\n        velocity.y += (-SPRING * offset.y - damping * velocity.y) * dt;\n        offset.x += velocity.x * dt;\n        offset.y += velocity.y * dt;\n        moving = true;\n      });\n\n      let typing = -1;\n      if (introducing && introKind === 'type') {\n        const step = 0.86 / glyphs.length;\n        const cursorAt = Math.min(glyphs.length - 1, Math.floor(progress / step));\n        typing = progress < 0.93 ? cursorAt : -1;\n        glyphs.forEach((glyph, i) => {\n          glyph.shown = i < cursorAt ? 1 : i === cursorAt ? smoothstep(0, step * 0.3, progress - i * step) : 0;\n        });\n      } else {\n        glyphs.forEach(glyph => {\n          glyph.shown = 1;\n        });\n      }\n\n      const focus = introducing ? typing : dragging >= 0 ? dragging : active ? glyphAt(lens.x, lens.y) : -1;\n      if (focus >= 0 && s.selection) {\n        const glyph = glyphs[focus];\n        const bx1 = glyph.box.x1 + glyph.offset.x - 6;\n        const by1 = glyph.box.y1 + glyph.offset.y - 6;\n        const bx2 = glyph.box.x2 + glyph.offset.x + 6;\n        const by2 = glyph.box.y2 + glyph.offset.y + 6;\n        if (frame.index < 0 || frame.alpha < 0.02) {\n          frame.x1 = bx1;\n          frame.y1 = by1;\n          frame.x2 = bx2;\n          frame.y2 = by2;\n        }\n        const glide = focus === dragging ? 0.02 : 0.08;\n        frame.x1 = approach(frame.x1, bx1, dt, glide);\n        frame.y1 = approach(frame.y1, by1, dt, glide);\n        frame.x2 = approach(frame.x2, bx2, dt, glide);\n        frame.y2 = approach(frame.y2, by2, dt, glide);\n        frame.index = focus;\n      }\n      frame.alpha = approach(frame.alpha, focus >= 0 && s.selection ? 1 : 0, dt, 0.1);\n\n      glyphs.forEach((glyph, i) => {\n        const target = introducing\n          ? introKind === 'type' && (i === typing || (typing >= 0 && i > typing))\n            ? 1\n            : 0\n          : s.reveal === 'letter' && i === focus && i !== dragging\n            ? 1\n            : 0;\n        if (introducing && glyph.shown === 0) glyph.outline = target;\n        glyph.outline = approach(glyph.outline, target, dt, 0.09);\n        if (Math.abs(glyph.outline - target) > 0.002) moving = true;\n        else glyph.outline = target;\n      });\n\n      guideAlpha = approach(guideAlpha, s.guides && !introducing && (focus >= 0 || presence > 0.4) ? 1 : 0, dt, 0.14);\n\n      const hovering = !introducing && focus >= 0 && pointer.inside;\n      const nextCursor =\n        dragging >= 0 ? 'grabbing' : hovering && s.draggable ? 'grab' : hovering && s.clickKick ? 'pointer' : '';\n      if (nextCursor !== cursor) {\n        cursor = nextCursor;\n        container.style.cursor = nextCursor;\n      }\n\n      ctx.setTransform(1, 0, 0, 1, 0, 0);\n      ctx.globalCompositeOperation = 'source-over';\n      ctx.globalAlpha = 1;\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\n      for (const glyph of glyphs) {\n        const moved = Math.hypot(glyph.offset.x, glyph.offset.y);\n        if (moved > 1) {\n          ctx.globalAlpha = Math.min(1, moved / 24) * 0.55;\n          blit(ctx, glyph.dashes, 0, 0, 0, 0);\n          ctx.globalAlpha = 1;\n        }\n      }\n      if (introducing && introKind === 'metrics') {\n        drawLines(s, view, accent, 1 - smoothstep(0.84, 1, progress), k =>\n          smoothstep(0.02 + 0.04 * k, 0.2 + 0.04 * k, progress)\n        );\n      } else if (guideAlpha > 0.005) {\n        drawLines(s, view, accent, guideAlpha * 0.6, () => 1);\n      }\n      glyphs.forEach((glyph, i) => {\n        if (introducing && introKind === 'assemble') {\n          ensureCells(glyph, view);\n          if (progress < glyph.assembled) {\n            drawAssembling(glyph, progress, accent);\n            return;\n          }\n        }\n        if (introducing && introKind === 'metrics') {\n          const lead = glyphs.length > 1 ? i / (glyphs.length - 1) : 0;\n          if (progress < 0.72 + 0.28 * lead) {\n            drawFilling(glyph, lead, progress, view);\n            return;\n          }\n        }\n        if (glyph.shown < 0.001) return;\n        if (glyph.outline < 0.999) {\n          ctx.globalAlpha = (1 - glyph.outline) * glyph.shown;\n          blit(ctx, glyph.fill, glyph.offset.x, glyph.offset.y, 0, 0);\n        }\n        if (glyph.outline > 0.001) {\n          ctx.globalAlpha = glyph.outline * glyph.shown;\n          blit(ctx, glyph.dashes, glyph.offset.x, glyph.offset.y, 0, 0);\n        }\n        ctx.globalAlpha = 1;\n      });\n      if (presence > 0.001) drawReveal(s);\n      drawFrame(s, accent);\n\n      const settling =\n        moving ||\n        introducing ||\n        kicks.length > 0 ||\n        (guideAlpha > 0.005 && guideAlpha < 0.995) ||\n        Math.abs(presence - (s.reveal === 'area' && active && dragging < 0 ? 1 : 0)) > 0.002 ||\n        (frame.alpha > 0.01 && frame.alpha < 0.99);\n      if ((active || settling) && visible && alive) raf = requestAnimationFrame(tick);\n    };\n\n    const wake = () => {\n      if (raf || !visible || !alive) return;\n      last = performance.now();\n      raf = requestAnimationFrame(tick);\n    };\n    wakeRef.current = wake;\n\n    const resize = () => {\n      sizeDirty = true;\n      wake();\n    };\n\n    const locate = e => {\n      const rect = container.getBoundingClientRect();\n      pointer.x = e.clientX - rect.left;\n      pointer.y = e.clientY - rect.top;\n    };\n    const onMove = e => {\n      locate(e);\n      pointer.inside = true;\n      wake();\n    };\n    const onLeave = () => {\n      if (dragging >= 0) return;\n      pointer.inside = false;\n      wake();\n    };\n    const onDown = e => {\n      locate(e);\n      pointer.inside = true;\n      const s = settingsRef.current;\n      const primary = e.pointerType !== 'mouse' || e.button === 0;\n      const index = introducing || !primary ? -1 : glyphAt(pointer.x, pointer.y);\n      press.x = pointer.x;\n      press.y = pointer.y;\n      press.time = performance.now();\n      press.index = index;\n      if (s?.draggable && index >= 0) {\n        dragging = index;\n        grab.x = pointer.x - glyphs[index].offset.x;\n        grab.y = pointer.y - glyphs[index].offset.y;\n        container.setPointerCapture?.(e.pointerId);\n      }\n      wake();\n    };\n    const onUp = e => {\n      if (dragging >= 0) {\n        dragging = -1;\n        container.releasePointerCapture?.(e.pointerId);\n        const rect = container.getBoundingClientRect();\n        pointer.inside =\n          e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;\n      }\n      if (\n        e.type === 'pointerup' &&\n        press.index >= 0 &&\n        settingsRef.current?.clickKick &&\n        !reducedMotion &&\n        performance.now() - press.time < 450\n      ) {\n        const rect = container.getBoundingClientRect();\n        if (Math.hypot(e.clientX - rect.left - press.x, e.clientY - rect.top - press.y) < 6) kick(press.index);\n      }\n      press.index = -1;\n      wake();\n    };\n\n    container.addEventListener('pointermove', onMove, { passive: true });\n    container.addEventListener('pointerenter', onMove, { passive: true });\n    container.addEventListener('pointerdown', onDown, { passive: true });\n    container.addEventListener('pointerup', onUp, { passive: true });\n    container.addEventListener('pointercancel', onUp, { passive: true });\n    container.addEventListener('pointerleave', onLeave, { passive: true });\n\n    const resizeObserver = new ResizeObserver(resize);\n    resizeObserver.observe(container);\n    window.addEventListener('resize', resize, { passive: true });\n    const intersectionObserver = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      wake();\n    });\n    intersectionObserver.observe(container);\n    const themeObserver = new MutationObserver(wake);\n    for (const node of [document.documentElement, document.body]) {\n      if (node) themeObserver.observe(node, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });\n    }\n    const schemeQuery = window.matchMedia?.('(prefers-color-scheme: dark)');\n    schemeQuery?.addEventListener?.('change', wake);\n    if (document.fonts) document.fonts.ready.then(fontsLoaded, fontsLoaded);\n\n    resize();\n\n    return () => {\n      alive = false;\n      cancelAnimationFrame(raf);\n      wakeRef.current = () => {};\n      resizeObserver.disconnect();\n      window.removeEventListener('resize', resize);\n      intersectionObserver.disconnect();\n      themeObserver.disconnect();\n      schemeQuery?.removeEventListener?.('change', wake);\n      container.style.cursor = '';\n      container.removeEventListener('pointermove', onMove);\n      container.removeEventListener('pointerenter', onMove);\n      container.removeEventListener('pointerdown', onDown);\n      container.removeEventListener('pointerup', onUp);\n      container.removeEventListener('pointercancel', onUp);\n      container.removeEventListener('pointerleave', onLeave);\n    };\n  }, []);\n\n  return (\n    <div ref={containerRef} className={`tech-text ${className}`.trim()} style={style} role=\"img\" aria-label={text}>\n      <canvas ref={canvasRef} className=\"tech-text-canvas\" />\n    </div>\n  );\n};\n\nexport default TechText;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}