{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tricone-bit-teeth",
  "title": "Tricone Bit Teeth",
  "description": "A full-bleed rock face worked by a rotary tricone bit: three cone cutters orbit the bit centre in a fixed 120-degree pattern, each tooth row stamping a crater cluster into the face that heals back to flat before the next pass, so the surface reads as a ring being continuously worked rather than a diffuse pockmarked plate.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/loud/tricone-bit-teeth/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// TriconeBitTeeth — a full-bleed hero built on a rotary tricone rock bit: the\n// three cone cutters at a bit's periphery orbit the borehole axis while each\n// spins on its own skewed journal, so their teeth both crush and scrape the\n// formation with every pass. Unlike edm-crater-field's steady-state RANDOM\n// discharge scatter, this crater field is never diffuse or uniform — every\n// dent lands only where a cone's tooth row actually swept, in three\n// simultaneous, 120-degree-offset clusters, so the field always reads as a\n// RING traced by a rotating tool rather than a pockmarked plate. The bit\n// bodies themselves are drawn as visible orbiting wedges so \"three cones\n// rotating\" is legible on its own, independent of the crater ring it leaves.\n//\n// The real bit rotates around 60-120 RPM and strikes 33 times/lap (3 cones x\n// 11 teeth) — both far too fast to paint 1:1 against a 60Hz page without\n// aliasing into a strobe (round 9's meter-matrix-scan lesson). Both rates are\n// deliberately decoupled: the bit is rendered orbiting at an 8-RPM-equivalent\n// sweep (one lap every 7.5s), and the 33 individual tooth strikes per lap are\n// collapsed into one 3-cone simultaneous cluster roughly every 680ms, which\n// is what a viewer can actually track landing and healing.\n//\n// IMPLEMENTATION: like edm-crater-field, the rock face is a small CPU-side\n// height/depth buffer (cells derived from the container's SMALLER dimension,\n// 96 cells across it) that strikes stamp a crater into and that decays back\n// toward flat every frame — never a monotonic fill, a resident population in\n// equilibrium. The buffer is painted to an offscreen ImageData canvas and\n// blitted, scaled and smoothed, onto the display canvas; the three cone\n// bodies and the mud-jet sweep ring are drawn on top in vector form each\n// frame so the \"rotating tool\" reading never depends on the field alone.\n//\n// Colours: three luminance stops derived from --background, --foreground and\n// --ns-muted (getComputedStyle at mount, re-read on a MutationObserver\n// watching documentElement's class) — no literals anywhere. --ns-accent is\n// never touched: there is no interactive climactic moment (an optional\n// pointer only brightens the swept ring in luminance), matching the showpiece\n// recipe's standing \"accent-tinted pointer highlight\" defect.\n// ---------------------------------------------------------------------------\n\nexport interface TriconeBitTeethProps {\n  /** Freezes the surface on a composed post-strike still frame without unmounting. */\n  paused?: boolean;\n  /** Rendered in the DOM over the field — eyebrow, headline, CTA. */\n  children?: React.ReactNode;\n  className?: string;\n  style?: React.CSSProperties;\n}\n\ntype RGB = [number, number, number];\n\nfunction parseHex(raw: string): RGB | null {\n  const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(raw.trim());\n  if (!m) return null;\n  let h = m[1];\n  if (h.length === 3) h = h.split(\"\").map((c) => c + c).join(\"\");\n  const n = parseInt(h, 16);\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\nfunction mixRGB(a: RGB, b: RGB, t: number): RGB {\n  return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];\n}\n\nfunction luminance([r, g, b]: RGB): number {\n  return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n}\n\nfunction rgbCss([r, g, b]: RGB, a = 1): string {\n  return `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${a})`;\n}\n\nconst CONES = 3;\nconst TEETH_PER_CONE = 11;\nconst LAP_SECONDS = 7.5; // 8-RPM-equivalent decoupled sweep, see header\nconst STRIKE_INTERVAL = 0.68; // s between simultaneous 3-cone strike clusters\nconst HEAL_RATE = 0.9; // depth *= exp(-HEAL_RATE * dt), ~90% refill in ~1.1s\nconst FIELD_CELLS = 96; // cells across the container's smaller dimension\nconst JET_PERIOD = 0.9; // s per mud-jet sweep lap\nconst JET_SPEED = 140; // px/s the sweep ring expands at\n\n// The frame drawn under prefers-reduced-motion (and paused): right after a\n// 3-cone simultaneous strike lands, at maximum unhealed depth — the single\n// most structured frame, showing the tooth pattern rather than a mid-heal\n// blur or the flat rock the loop would otherwise freeze on.\nconst STATIC_STRIKE_PHASE = 0; // seconds past the most recent strike event\n\nexport function TriconeBitTeeth({\n  paused = false,\n  children,\n  className = \"\",\n  style,\n}: TriconeBitTeethProps) {\n  const wrapRef = useRef<HTMLDivElement | null>(null);\n  const canvasRef = useRef<HTMLCanvasElement | null>(null);\n  const uid = useId();\n  const pausedRef = useRef(paused);\n  pausedRef.current = paused;\n\n  useEffect(() => {\n    const wrap = wrapRef.current;\n    const canvas = canvasRef.current;\n    if (!wrap || !canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    let cssW = 0;\n    let cssH = 0;\n    let dpr = 1;\n    let disposed = false;\n    let running = false;\n    let raf = 0;\n    let staticMode = false;\n    let lastMs = performance.now();\n    let simTime = 0;\n    let lastStrikeAt = -STRIKE_INTERVAL;\n\n    let fieldW = FIELD_CELLS;\n    let fieldH = FIELD_CELLS;\n    let depth = new Float32Array(fieldW * fieldH);\n    const field = document.createElement(\"canvas\");\n    const fctx = field.getContext(\"2d\", { willReadFrequently: true });\n    let fieldImg: ImageData | null = null;\n\n    let bg: RGB = [1, 1, 1];\n    let raised: RGB = [0.86, 0.86, 0.86];\n    let struckDeep: RGB = [0.06, 0.06, 0.06];\n    let muted: RGB = [0.55, 0.55, 0.55];\n\n    const readColors = () => {\n      const cs = getComputedStyle(document.documentElement);\n      const bgTok = parseHex(cs.getPropertyValue(\"--background\")) ?? [1, 1, 1];\n      const fg = parseHex(cs.getPropertyValue(\"--foreground\")) ?? [0.09, 0.09, 0.09];\n      const mut = parseHex(cs.getPropertyValue(\"--ns-muted\")) ?? [0.55, 0.55, 0.55];\n      bg = bgTok;\n      muted = mut;\n      // dark theme: unstruck rock sits slightly above background; struck\n      // craters read near-black. Light theme needs the same three legible\n      // steps compressed into far less headroom below --background, so the\n      // dark anchor is pulled from --foreground rather than pure black.\n      if (luminance(bgTok) < 0.5) {\n        raised = mixRGB(bgTok, [1, 1, 1], 0.14);\n        struckDeep = mixRGB(bgTok, [0, 0, 0], 0.55);\n      } else {\n        raised = mixRGB(bgTok, fg, 0.1);\n        struckDeep = mixRGB(fg, [0, 0, 0], 0.25);\n      }\n    };\n    readColors();\n\n    const setFieldRes = () => {\n      const ref = Math.min(cssW, cssH) || 1;\n      fieldW = FIELD_CELLS;\n      fieldH = Math.max(24, Math.round(FIELD_CELLS * (cssH / ref)));\n      const w2 = Math.max(24, Math.round(FIELD_CELLS * (cssW / ref)));\n      fieldW = w2;\n      depth = new Float32Array(fieldW * fieldH);\n      field.width = fieldW;\n      field.height = fieldH;\n      fieldImg = fctx ? fctx.createImageData(fieldW, fieldH) : null;\n    };\n\n    // A crater is a recessed bowl: a soft radial falloff stamped additively\n    // into the depth buffer (0 = flat, up to 1 = deepest fresh crater).\n    const stampCrater = (cx: number, cy: number, radiusCells: number, amp: number) => {\n      const r = Math.max(1, radiusCells);\n      const x0 = Math.max(0, Math.floor(cx - r));\n      const x1 = Math.min(fieldW - 1, Math.ceil(cx + r));\n      const y0 = Math.max(0, Math.floor(cy - r));\n      const y1 = Math.min(fieldH - 1, Math.ceil(cy + r));\n      for (let y = y0; y <= y1; y++) {\n        for (let x = x0; x <= x1; x++) {\n          const dx = (x - cx) / r;\n          const dy = (y - cy) / r;\n          const d2 = dx * dx + dy * dy;\n          if (d2 > 1) continue;\n          const fall = Math.exp(-d2 * 2.2);\n          const idx = y * fieldW + x;\n          depth[idx] = Math.min(1, depth[idx] + amp * fall);\n        }\n      }\n    };\n\n    // The three cones sit 120 degrees apart at the bit's periphery and orbit\n    // the bit centre together. Each cone's tooth row strikes a cluster of\n    // TEETH_PER_CONE small craters scattered along a short arc of its own\n    // track — this is what keeps the ring textured (individual tooth marks)\n    // rather than a single smeared dent per cone per strike.\n    const strikeCluster = (bitAngle: number) => {\n      const ref = Math.min(fieldW, fieldH);\n      const orbitR = ref * 0.32;\n      const cx = fieldW / 2;\n      const cy = fieldH / 2;\n      for (let c = 0; c < CONES; c++) {\n        const coneAngle = bitAngle + (c * Math.PI * 2) / CONES;\n        const coneCx = cx + Math.cos(coneAngle) * orbitR;\n        const coneCy = cy + Math.sin(coneAngle) * orbitR;\n        for (let t = 0; t < TEETH_PER_CONE; t++) {\n          // teeth spread across a short arc of the cone's own track, not a\n          // single point, so one strike cluster reads as a row, not a dot\n          const spread = ((t / (TEETH_PER_CONE - 1)) - 0.5) * 0.5;\n          const ta = coneAngle + spread;\n          const tr = orbitR * (0.94 + 0.09 * ((t % 3) - 1));\n          const tx = cx + Math.cos(ta) * tr;\n          const ty = cy + Math.sin(ta) * tr;\n          stampCrater(tx, ty, ref * 0.028, 0.75 + 0.25 * Math.random());\n        }\n      }\n    };\n\n    const paintField = () => {\n      if (!fctx || !fieldImg) return;\n      const data = fieldImg.data;\n      for (let i = 0; i < depth.length; i++) {\n        const d = depth[i];\n        // raised (unstruck) rock at d=0 down to the deepest struck stop at d=1\n        const col = d <= 0.001 ? raised : mixRGB(raised, struckDeep, Math.min(1, d));\n        const j = i * 4;\n        data[j] = Math.round(col[0] * 255);\n        data[j + 1] = Math.round(col[1] * 255);\n        data[j + 2] = Math.round(col[2] * 255);\n        data[j + 3] = 255;\n      }\n      fctx.putImageData(fieldImg, 0, 0);\n    };\n\n    const drawCones = (t: number, bitAngle: number) => {\n      const ref = Math.min(cssW, cssH);\n      const cx = cssW / 2;\n      const cy = cssH / 2;\n      const orbitR = ref * 0.32;\n      const bodyR = ref * 0.1;\n      for (let c = 0; c < CONES; c++) {\n        const coneAngle = bitAngle + (c * Math.PI * 2) / CONES;\n        const px = cx + Math.cos(coneAngle) * orbitR;\n        const py = cy + Math.sin(coneAngle) * orbitR;\n        const spin = t * 6.0 + c; // decorative own-axis spin, purely visual\n        ctx.save();\n        ctx.translate(px, py);\n        ctx.rotate(spin);\n        ctx.beginPath();\n        ctx.moveTo(0, -bodyR);\n        ctx.lineTo(bodyR * 0.82, bodyR * 0.62);\n        ctx.lineTo(-bodyR * 0.82, bodyR * 0.62);\n        ctx.closePath();\n        ctx.fillStyle = rgbCss(muted, 0.9);\n        ctx.fill();\n        ctx.strokeStyle = rgbCss(struckDeep, 0.7);\n        ctx.lineWidth = Math.max(1, bodyR * 0.05);\n        ctx.stroke();\n        ctx.restore();\n      }\n    };\n\n    const drawJetSweep = (t: number) => {\n      const ref = Math.min(cssW, cssH);\n      const maxR = ref * 0.46;\n      const phase = ((t % JET_PERIOD) / JET_PERIOD) * (JET_SPEED * JET_PERIOD);\n      const r = Math.min(maxR, phase);\n      const alpha = 0.08 * (1 - r / maxR);\n      if (alpha <= 0.002) return;\n      ctx.save();\n      ctx.strokeStyle = rgbCss(mixRGB(raised, [1, 1, 1], 0.5), alpha);\n      ctx.lineWidth = Math.max(2, ref * 0.02);\n      ctx.beginPath();\n      ctx.arc(cssW / 2, cssH / 2, r, 0, Math.PI * 2);\n      ctx.stroke();\n      ctx.restore();\n    };\n\n    const draw = () => {\n      if (cssW <= 0 || cssH <= 0 || !fctx) return;\n      const t = staticMode ? lastStrikeAt + STATIC_STRIKE_PHASE : simTime;\n      const bitAngle = (t / LAP_SECONDS) * Math.PI * 2;\n\n      paintField();\n      ctx.save();\n      ctx.imageSmoothingEnabled = true;\n      ctx.fillStyle = rgbCss(raised);\n      ctx.fillRect(0, 0, cssW, cssH);\n      ctx.drawImage(field, 0, 0, cssW, cssH);\n      ctx.restore();\n\n      drawJetSweep(t);\n      drawCones(t, bitAngle);\n    };\n\n    const loop = (nowMs: number) => {\n      const rawMs = nowMs - lastMs;\n      const dt = Math.min(0.05, Math.max(0, rawMs / 1000));\n      lastMs = nowMs;\n      simTime += dt;\n\n      // heal every resident crater back toward flat, framerate-independent\n      const heal = Math.exp(-HEAL_RATE * dt);\n      for (let i = 0; i < depth.length; i++) depth[i] *= heal;\n\n      const bitAngle = (simTime / LAP_SECONDS) * Math.PI * 2;\n      if (simTime - lastStrikeAt >= STRIKE_INTERVAL) {\n        lastStrikeAt = simTime;\n        strikeCluster(bitAngle);\n      }\n\n      draw();\n      raf = requestAnimationFrame(loop);\n    };\n\n    const wake = () => {\n      if (running || disposed) return;\n      running = true;\n      lastMs = performance.now();\n      raf = requestAnimationFrame(loop);\n    };\n    const sleep = () => {\n      cancelAnimationFrame(raf);\n      running = false;\n    };\n\n    const dprCap = () => Math.min(window.devicePixelRatio || 1, 2);\n\n    const applyBacking = () => {\n      if (cssW < 2 || cssH < 2) return;\n      dpr = dprCap();\n      const pw = Math.round(cssW * dpr);\n      const ph = Math.round(cssH * dpr);\n      if (canvas.width !== pw || canvas.height !== ph) {\n        canvas.width = pw;\n        canvas.height = ph;\n      }\n      canvas.style.width = `${cssW}px`;\n      canvas.style.height = `${cssH}px`;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n    };\n\n    const resize = () => {\n      const rect = wrap.getBoundingClientRect();\n      if (rect.width < 2 || rect.height < 2) return;\n      cssW = rect.width;\n      cssH = rect.height;\n      applyBacking();\n      setFieldRes();\n      // seed a struck ring immediately so a freshly-mounted/resized surface\n      // never shows a blank flat frame while waiting for the first cycle\n      strikeCluster(0);\n      strikeCluster((Math.PI * 2) / LAP_SECONDS / 3);\n      draw();\n    };\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(wrap);\n    resize();\n\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    let reduced = mq.matches;\n    const applyMode = () => {\n      if (reduced || pausedRef.current) {\n        staticMode = true;\n        sleep();\n        draw();\n      } else {\n        staticMode = false;\n        wake();\n      }\n    };\n    const onMq = () => {\n      reduced = mq.matches;\n      applyMode();\n    };\n    mq.addEventListener(\"change\", onMq);\n\n    let onScreen = true;\n    const io = new IntersectionObserver(\n      (entries) => {\n        onScreen = entries.some((en) => en.isIntersecting);\n        if (!onScreen) sleep();\n        else if (!staticMode && !document.hidden) wake();\n      },\n      { threshold: 0 }\n    );\n    io.observe(wrap);\n\n    const onVis = () => {\n      if (document.hidden) sleep();\n      else if (!staticMode && onScreen) wake();\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    const themeObserver = new MutationObserver(() => {\n      readColors();\n      if (staticMode) draw();\n    });\n    themeObserver.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    let lastPolledPaused = pausedRef.current;\n    let poll = 0;\n    const tick = () => {\n      if (pausedRef.current !== lastPolledPaused) {\n        lastPolledPaused = pausedRef.current;\n        applyMode();\n      }\n      poll = window.setTimeout(tick, 140);\n    };\n    tick();\n\n    applyMode();\n\n    return () => {\n      disposed = true;\n      ro.disconnect();\n      io.disconnect();\n      mq.removeEventListener(\"change\", onMq);\n      document.removeEventListener(\"visibilitychange\", onVis);\n      themeObserver.disconnect();\n      window.clearTimeout(poll);\n      sleep();\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  return (\n    <div\n      ref={wrapRef}\n      data-tricone-bit-teeth={uid}\n      className={`relative isolate h-full w-full overflow-hidden bg-background ${className}`}\n      style={style}\n    >\n      <canvas ref={canvasRef} aria-hidden=\"true\" className=\"absolute inset-0 block h-full w-full\" />\n      {children ? <div className=\"relative z-[1] h-full w-full\">{children}</div> : null}\n    </div>\n  );\n}\n\nTriconeBitTeeth.displayName = \"TriconeBitTeeth\";\n",
      "type": "registry:ui",
      "target": "components/ui/tricone-bit-teeth.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "loud",
    "tags": [
      "background",
      "hero",
      "canvas",
      "rock",
      "drilling",
      "erosion",
      "decorative"
    ],
    "instruction": "Build <TriconeBitTeeth paused? children? className? style?> as a full-bleed 2D-canvas hero built on a rotary tricone rock bit: three cone cutters orbit the bit's centre together (bitAngle = t / LAP_SECONDS * 2*PI, LAP_SECONDS=7.5, an 8-RPM-equivalent sweep deliberately decoupled from the real 60-120 RPM rate per the round 9 aliasing rule) while a CPU-side depth buffer records where their teeth have struck. REAL NUMBERS: 3 cones, 120 degrees apart, orbit radius = 0.32 * min(fieldW, fieldH); TEETH_PER_CONE=11 teeth per cone, spread across a short arc of the cone's own track (not a single point) so one strike cluster reads as a row; a simultaneous 3-cone STRIKE_INTERVAL=0.68s cluster (33 real tooth-strikes/lap collapsed into one legible 3-cone event, per the round 9 'roughly a second between discrete events' rule); each tooth stamps a soft radial-falloff crater (exp(-d^2*2.2) within radius ref*0.028) at amplitude 0.75-1.0 into a Float32Array depth buffer sized FIELD_CELLS=96 cells across the container's SMALLER dimension (aspect-scaled on the other axis); every real frame the WHOLE buffer decays via depth *= exp(-HEAL_RATE*dt), HEAL_RATE=0.9/s (~90% refill in ~1.1s) — birth and decay are both constant and independent, so the ring is a resident population in equilibrium, never a monotonic fill (same steady-state discipline as registry/loud/edm-crater-field, but here births are STRUCTURED — always at the three cones' current orbit position — never uniform-random, which is what keeps this from reading as edm-crater-field's diffuse plate). RENDERING: the depth buffer is painted into an offscreen low-res canvas each frame (raised rock at depth<=0 mixed toward a struck-deep stop as depth approaches 1, both derived from --background/--foreground) and blitted scaled-up with imageSmoothingEnabled=true onto the display canvas — a soft, non-pixel-hard crater read appropriate to rock rather than a machined plate. The three cone BODIES are drawn as small triangular wedges directly on the display canvas each frame, orbiting at bitAngle with an additional decorative own-axis spin (t*6 rad/s, cosmetic only, not tied to any real number) — this vector overlay is what makes 'three cones rotating' legible on its own, independent of the crater ring, and is the deliberate differentiator from edm-crater-field's shader-only height field with no visible tool. A faint mud-jet sweep ring (0.08 peak luminance, radiating from bit centre at JET_SPEED=140px/s, resetting every JET_PERIOD=0.9s) stands in for nozzle discharge and must stay under the crater ring in read priority — a wash, not a highlight. MUST NOT: tint the crater ring, cone bodies or jet sweep with --ns-accent — there is no interactive control on this component (a pointer, if ever added, may only brighten the ring in luminance near the cursor), matching the showpiece recipe's standing 'accent-tinted pointer highlight' defect (edge-yield, granule-churn, shear-billow). RESTING LOOP: t0 shows an already-mid-cycle ring (seeded by two immediate strikeCluster() calls in resize(), at bit angles 0 and one-third of a lap, so mount never shows a blank flat frame); by 2.5s roughly 3-4 more clusters have landed and one or two early clusters have healed back toward flat; by 5s the ring shows continuous turnover — fresh strikes at the leading edge, healed craters fading at the trailing edge — which is the 'alive at rest' proof, not a filling-in or emptying-out pattern. TOKENS: three luminance stops (raised rock, struck-deep, muted) derived from --background, --foreground, --ns-muted via getComputedStyle(document.documentElement) at mount, re-read on a MutationObserver watching documentElement's class — NO PAINT BEFORE THE FIRST READ, readColors() runs before the first resize()/draw(). Light theme (checked first): the dark anchor for struck-deep is mixed from --foreground rather than pure black, since light theme has far less headroom below --background than dark theme does above it — verify the raised/struck-deep/muted three-step ramp stays legible there specifically. `ResizeObserver` reallocates the depth buffer and reseeds two clusters; `IntersectionObserver` (threshold 0) and `visibilitychange` both pause the single rAF loop; `prefers-reduced-motion`/`paused` freeze on STATIC_STRIKE_PHASE — the instant right after the most recent strike lands, at maximum unhealed depth, the single most structured frame in the loop. Canvas is `aria-hidden` decoration with `w-full h-full`; children render over the field in a `relative z-[1]` layer and need a token scrim, since the crater ring can pass under any line of overlaid type."
  },
  "type": "registry:ui"
}