{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "TechText-TS-CSS",
	"title": "TechText",
	"description": "A wordmark whose letters turn into dashed vector paths under the cursor. Grab any letter to drag it off the baseline and it springs back home.",
	"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.tsx",
			"content": "'use client';\n\nimport { useEffect, useRef } from 'react';\nimport type { CSSProperties } from 'react';\n\nimport './TechText.css';\n\ntype Box = { x1: number; y1: number; x2: number; y2: number };\ntype Art = { image: HTMLCanvasElement; left: number; top: number };\ntype Shape = { char: string; x: number; box: Box };\ntype Glyph = Shape & {\n  offset: { x: number; y: number };\n  velocity: { x: number; y: number };\n  outline: number;\n  index: number;\n  fill: Art;\n  dashes: Art;\n};\ntype Word = { size: number; baseline: number; left: number; right: number; top: number; bottom: number };\n\nexport interface TechTextProps {\n  text?: string;\n  fontFamily?: string;\n  fontWeight?: number;\n  fontSize?: number;\n  letterSpacing?: number;\n  color?: string;\n  accentColor?: string;\n  reach?: number;\n  softness?: number;\n  dashLength?: number;\n  dashGap?: number;\n  strokeWidth?: number;\n  lineStyle?: 'dashed' | 'solid';\n  reveal?: 'area' | 'letter' | 'off';\n  specks?: number;\n  selection?: boolean;\n  labels?: boolean;\n  draggable?: boolean;\n  sweep?: boolean;\n  speed?: number;\n  className?: string;\n  style?: CSSProperties;\n}\n\ntype Settings = Required<Omit<TechTextProps, 'className' | 'style'>>;\n\nconst LABEL_FONT = '10px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace';\nconst FALLOFF_STEPS = 8;\nconst SPRING = 320;\nconst DAMPING = 22;\n\nconst approach = (current: number, target: number, dt: number, seconds: number) =>\n  current + (target - current) * (1 - Math.exp(-dt / seconds));\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n  let h = String(hex || '').replace('#', '');\n  if (h.length === 3) h = h.replace(/./g, c => c + c);\n  const n = parseInt(h.slice(0, 6), 16);\n  return Number.isNaN(n) ? [255, 255, 255] : [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n};\n\nconst rgba = (hex: string, alpha: number) => {\n  const [r, g, b] = hexToRgb(hex);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n};\n\nconst noise = (...values: number[]) => {\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: number) => (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 = '#ffffff',\n  accentColor = '#ffffff',\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  sweep = true,\n  speed = 1,\n  className = '',\n  style\n}: TechTextProps) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const settingsRef = useRef<Settings | null>(null);\n  const wakeRef = useRef<() => void>(() => {});\n\n  useEffect(() => {\n    settingsRef.current = {\n      text,\n      fontFamily,\n      fontWeight,\n      fontSize,\n      letterSpacing,\n      color,\n      accentColor,\n      reach,\n      softness,\n      dashLength,\n      dashGap,\n      strokeWidth,\n      lineStyle,\n      reveal,\n      specks,\n      selection,\n      labels,\n      draggable,\n      sweep,\n      speed\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 = true;\n    let alive = true;\n    let layoutKey = '';\n    let requestedFont = '';\n    let word: Word | null = null;\n    let glyphs: Glyph[] = [];\n    let presence = 0;\n    let clock = 0;\n    let pulse = 0;\n    let placed = false;\n    let dragging = -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 family = (s: Settings) => s.fontFamily || getComputedStyle(container).fontFamily || 'sans-serif';\n    const fontFor = (s: Settings, size: number) => `${s.fontWeight} ${size}px ${family(s)}`;\n\n    const setFont = (target: CanvasRenderingContext2D, s: Settings, size: number) => {\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: Settings, view: Word, glyph: Shape, stroke: boolean): Art => {\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 = s.color;\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 = s.color;\n        c.fillText(glyph.char, glyph.x, view.baseline);\n      }\n      return { image, left, top };\n    };\n\n    const ensureLayout = (s: Settings): Word => {\n      const key = [\n        s.text,\n        family(s),\n        s.fontWeight,\n        s.fontSize,\n        s.letterSpacing,\n        s.color,\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 x = (width - inkWidth) / 2 + m.actualBoundingBoxLeft;\n      const baseline = (height - inkHeight) / 2 + m.actualBoundingBoxAscent;\n      const next = {\n        size,\n        baseline,\n        left: x - m.actualBoundingBoxLeft,\n        right: x + m.actualBoundingBoxRight,\n        top: baseline - m.actualBoundingBoxAscent,\n        bottom: baseline + m.actualBoundingBoxDescent\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        glyphs.push({\n          ...base,\n          offset: kept?.char === char ? kept.offset : { x: 0, y: 0 },\n          velocity: { x: 0, y: 0 },\n          outline: 0,\n          index: i,\n          fill: sprite(s, next, base, false),\n          dashes: sprite(s, next, base, true)\n        });\n      });\n      dragging = -1;\n      frame.index = -1;\n      return next;\n    };\n\n    const glyphAt = (x: number, y: number) => {\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 = (\n      target: CanvasRenderingContext2D,\n      cx: number,\n      cy: number,\n      radius: number,\n      strength: number,\n      softness: number\n    ) => {\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 = (\n      target: CanvasRenderingContext2D,\n      art: Art,\n      dx: number,\n      dy: number,\n      originX: number,\n      originY: number\n    ) => {\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: Settings) => {\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: number) => (Math.round(value * dpr) + 0.5) / dpr;\n\n    const perimeterPoint = (distance: number, w: number, h: number): [number, number, number, number] => {\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: Settings, a: number) => {\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\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        if (tone < 0.26 || (large && tone < 0.78)) {\n          ctx.strokeStyle = rgba(s.accentColor, alpha);\n          ctx.strokeRect(left + 0.5, top + 0.5, size, size);\n          if (large && tone > 0.5) {\n            ctx.fillStyle = rgba(s.accentColor, alpha);\n            ctx.fillRect(Math.round(x) - 1, Math.round(y) - 1, 2, 2);\n          }\n        } else {\n          ctx.fillStyle = rgba(s.accentColor, alpha);\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.fillStyle = rgba(s.accentColor, 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    };\n\n    const drawFrame = (s: Settings) => {\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\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.lineWidth = 1;\n        ctx.strokeStyle = rgba(s.accentColor, 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.fillStyle = rgba(s.accentColor, 0.7 * a);\n        ctx.fill();\n      }\n\n      ctx.beginPath();\n      ctx.rect(x1, y1, x2 - x1, y2 - y1);\n      ctx.lineWidth = 1;\n      ctx.strokeStyle = rgba(s.accentColor, 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.fillStyle = rgba(s.accentColor, 0.95 * a);\n      ctx.fill();\n\n      if (s.specks > 0) {\n        ctx.lineWidth = 1;\n        drawSpecks(s, a);\n      }\n\n      if (!s.labels) return;\n      ctx.font = LABEL_FONT;\n      ctx.textAlign = 'left';\n      ctx.textBaseline = 'bottom';\n      ctx.fillStyle = rgba(s.accentColor, 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    };\n\n    const tick = (now: number) => {\n      raf = 0;\n      const s = settingsRef.current;\n      if (!s) return;\n      const dt = Math.min(0.05, Math.max(0.001, (now - last) / 1000));\n      last = now;\n      const view = ensureLayout(s);\n\n      const sweeping = 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 = 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      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      const focus = 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 = s.reveal === 'letter' && i === focus && i !== dragging ? 1 : 0;\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      if (s.draggable) container.style.cursor = dragging >= 0 ? 'grabbing' : focus >= 0 && pointer.inside ? 'grab' : '';\n\n      ctx.setTransform(1, 0, 0, 1, 0, 0);\n      ctx.globalCompositeOperation = 'source-over';\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      for (const glyph of glyphs) {\n        if (glyph.outline < 0.999) {\n          ctx.globalAlpha = 1 - glyph.outline;\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;\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);\n\n      const settling =\n        moving ||\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      width = Math.max(1, container.clientWidth);\n      height = Math.max(1, container.clientHeight);\n      dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = Math.round(width * dpr);\n      canvas.height = Math.round(height * dpr);\n      layoutKey = '';\n      wake();\n    };\n\n    const locate = (e: PointerEvent) => {\n      const rect = container.getBoundingClientRect();\n      pointer.x = e.clientX - rect.left;\n      pointer.y = e.clientY - rect.top;\n    };\n    const onMove = (e: PointerEvent) => {\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: PointerEvent) => {\n      locate(e);\n      pointer.inside = true;\n      const s = settingsRef.current;\n      if (s?.draggable && (e.pointerType !== 'mouse' || e.button === 0)) {\n        const index = glyphAt(pointer.x, pointer.y);\n        if (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      }\n      wake();\n    };\n    const onUp = (e: PointerEvent) => {\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      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    const intersectionObserver = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      wake();\n    });\n    intersectionObserver.observe(container);\n    if (document.fonts) document.fonts.ready.then(refreshFonts, refreshFonts);\n\n    resize();\n\n    return () => {\n      alive = false;\n      cancelAnimationFrame(raf);\n      wakeRef.current = () => {};\n      resizeObserver.disconnect();\n      intersectionObserver.disconnect();\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": []
}