{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bug-dock",
  "title": "Bug Dock",
  "description": "\"Playing on every screen you ship.\" with a small broadcast logo bug docking at the bottom.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/bug-dock/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadManrope } from \"@remotion/google-fonts/Manrope\";\nimport { loadVariableFont } from \"@remotion/google-fonts/Urbanist\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst urbanist = loadVariableFont(\"normal\", { subsets: [\"latin\"] });\nconst manrope = loadManrope(\"normal\", {\n  weights: [\"500\", \"600\"],\n  subsets: [\"latin\"],\n});\n\nconst REMOCN_R_PATH =\n  \"M 0.01 0.81 C 0.01 1.73, 0.36 2.79, 1.09 4.13 C 4.91 11.04, 13.45 17.16, 21.7 18.9 C 22.94 19.16, 23.18 19.16, 51.39 19.27 C 76.07 19.36, 80.02 19.4, 81.32 19.57 C 89.89 20.69, 96.2 24.68, 99.38 31.01 C 103.19 38.56, 102.53 50.1, 97.91 57.07 C 94.66 61.96, 89.68 64.99, 83.26 66 C 82.81 66.07, 70.18 66.83, 55.2 67.69 C 24.82 69.43, 27.03 69.23, 24.18 70.4 C 19.9 72.15, 14.84 75.7, 10.65 79.89 C 4.86 85.68, 1.3 91.91, 0.25 98.13 C 0.12 98.85, 0.08 103.13, 0.04 116.66 L 0 134.26 9.5 134.26 L 19 134.26 19.05 119.41 C 19.1 103.27, 19.08 103.82, 19.82 101.04 C 21.79 93.65, 27.86 88.75, 36.45 87.63 C 37.23 87.53, 39.41 87.5, 43.57 87.53 C 50.12 87.59, 50.75 87.65, 53.22 88.45 C 56.61 89.56, 59.67 91.86, 62.02 95.07 C 62.52 95.76, 69.35 103.85, 77.2 113.07 C 85.04 122.28, 91.63 130.04, 91.85 130.32 C 92.07 130.59, 92.5 131.34, 92.82 131.97 C 93.52 133.35, 94.11 133.93, 95.06 134.13 C 95.5 134.23, 98.97 134.26, 106.36 134.23 L 117.01 134.19 100.82 113.07 C 91.91 101.45, 84.52 91.78, 84.39 91.57 C 83.36 89.89, 83.66 87.53, 85.09 86 C 85.79 85.25, 86.36 84.94, 88.07 84.38 C 96.18 81.72, 104.15 76.62, 109.97 70.36 C 120.59 58.93, 124.06 44.32, 119.44 30.43 C 114.59 15.81, 101.93 4.02, 87.64 0.83 C 83.93 0.01, 88.09 0.08, 41.85 0.04 L 0.01 0 0.01 0.81 Z\";\nconst R_RATIO = 124.06 / 134.26;\nconst R_EM = 0.76;\nconst R_GAP = 0.04;\nconst LOGO_TRACK = -0.025;\n\nexport interface BugDockProps {\n  name?: string;\n  headline?: string;\n  highlight?: string;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: { bg: \"#0B0B0B\", ink: \"#FFFFFF\", blue: \"#2F6BFF\" },\n  light: { bg: \"#FFFFFF\", ink: \"#0A0A0A\", blue: \"#1F4FE0\" },\n} as const;\n\nconst ENTER_AT = 0;\nconst TUCK_AT = 34;\nconst TUCK_STEP = 6;\nconst TUCK_DUR = 11;\nconst DOCK_DUR = 30;\nconst HEAD_DELAY = 14;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_TUCK = Easing.bezier(0.55, 0, 0.2, 1);\nconst EASE_DOCK = Easing.bezier(0.7, 0, 0.2, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\ninterface Metrics {\n  adv: number[];\n  baseline: number;\n  cap: number;\n}\n\nfunction measure(letters: string[], logo: boolean): Metrics {\n  const canvas = document.createElement(\"canvas\");\n  const ctx = canvas.getContext(\"2d\");\n  const adv: number[] = [];\n  let baseline = 0.78;\n  let cap = 0.72;\n  if (ctx) {\n    const family = logo ? manrope.fontFamily : urbanist.fontFamily;\n    const weight = logo ? 600 : 500;\n    ctx.font = `${weight} 200px \"${family}\"`;\n    for (const ch of letters) {\n      adv.push(ctx.measureText(ch).width / 200 + LOGO_TRACK);\n    }\n    const fm = ctx.measureText(\"H\");\n    const asc = fm.fontBoundingBoxAscent / 200;\n    const desc = fm.fontBoundingBoxDescent / 200;\n    baseline = (1 - (asc + desc)) / 2 + asc;\n    cap = fm.actualBoundingBoxAscent / 200;\n  }\n  return { adv, baseline, cap };\n}\n\nfunction useMetrics(letters: string[], logo: boolean) {\n  const key = `${logo ? 1 : 0}${letters.join(\"\")}`;\n  const [state, setState] = useState<{ key: string; m: Metrics } | null>(null);\n  const [handle] = useState(() => delayRender(\"bug-dock: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    const isLogo = key[0] === \"1\";\n    const chars = Array.from(key.slice(1));\n    Promise.all([\n      urbanist.waitUntilDone(),\n      manrope.waitUntilDone(),\n      document.fonts.load(`600 100px \"${manrope.fontFamily}\"`),\n      document.fonts.load(`500 100px \"${urbanist.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        setState({ key, m: measure(chars, isLogo) });\n        if (!released.current) {\n          released.current = true;\n          continueRender(handle);\n        }\n      });\n    return () => {\n      live = false;\n    };\n  }, [key, handle]);\n  return state && state.key === key ? state.m : null;\n}\n\nfunction RMark({ height, color }: { height: number; color: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 124.06 134.26\"\n      height={height}\n      width={height * R_RATIO}\n      style={{ display: \"block\" }}\n      aria-hidden\n    >\n      <path fill={color} d={REMOCN_R_PATH} />\n    </svg>\n  );\n}\n\nfunction normalizeHex(c: string | undefined, fallback: string) {\n  if (c && /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(c)) return c;\n  return fallback;\n}\n\nexport function BugDock({\n  name = \"remocn\",\n  headline = \"Playing on every screen you ship.\",\n  highlight = \"every screen\",\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: BugDockProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const c = THEMES[theme];\n  const blue = normalizeHex(accent, c.blue);\n\n  const clean = name.trim() || \"remocn\";\n  const logo = clean.toLowerCase() === \"remocn\";\n  const tail = useMemo(\n    () => Array.from(logo ? \"emocn\" : clean.slice(1)),\n    [logo, clean],\n  );\n  const head = logo ? \"\" : Array.from(clean)[0];\n  const letters = useMemo(\n    () => (logo ? tail : [head, ...tail]),\n    [logo, tail, head],\n  );\n  const m = useMetrics(letters, logo);\n\n  const transparent = background === \"transparent\";\n  const stage = transparent ? \"transparent\" : (background ?? c.bg);\n\n  const words = useMemo(\n    () => headline.split(/\\s+/).filter(Boolean),\n    [headline],\n  );\n  const hiWords = useMemo(\n    () =>\n      new Set(\n        highlight\n          .toLowerCase()\n          .split(/\\s+/)\n          .filter(Boolean)\n          .map((w) => w.replace(/[^\\p{L}\\p{N}]/gu, \"\")),\n      ),\n    [highlight],\n  );\n\n  if (!m) {\n    return (\n      <div style={{ position: \"absolute\", inset: 0, background: stage }} />\n    );\n  }\n\n  const portrait = H > W * 1.1;\n  const s = portrait ? W / 1080 : Math.min(W / 1280, H / 720);\n\n  const tailAdv = logo ? m.adv : m.adv.slice(1);\n  const firstAdv = logo ? R_EM * R_RATIO + R_GAP : m.adv[0];\n  const fullEm = firstAdv + tailAdv.reduce((a, b) => a + b, 0);\n  const LF = Math.min(\n    (W * (portrait ? 0.8 : 0.58)) / fullEm,\n    H * (portrait ? 0.14 : 0.3),\n  );\n  const baseY = H * 0.5 + (m.cap * LF) / 2;\n\n  const n = tailAdv.length;\n  const tuckP = tailAdv.map((_, k) => {\n    const order = n - 1 - k;\n    const at = TUCK_AT + order * TUCK_STEP;\n    return interpolate(t, [at, at + TUCK_DUR], [0, 1], {\n      ...CLAMP,\n      easing: EASE_TUCK,\n    });\n  });\n  const tuckEnd = TUCK_AT + (n - 1) * TUCK_STEP + TUCK_DUR;\n  const dockAt = tuckEnd + 2;\n  const headAt = dockAt + HEAD_DELAY;\n\n  const visibleEm =\n    firstAdv -\n    (logo ? R_GAP : 0) * tuckP[0] +\n    tailAdv.reduce((a, w, k) => a + w * (1 - tuckP[k]), 0);\n  const groupLeft = (W - visibleEm * LF) / 2;\n\n  const xs: number[] = [];\n  let shift = 0;\n  for (let k = 0; k < n; k++) {\n    let orig = firstAdv;\n    for (let j = 0; j < k; j++) orig += tailAdv[j];\n    shift += tuckP[k] * tailAdv[k];\n    xs.push(groupLeft + (orig - shift) * LF);\n  }\n\n  const dock = interpolate(t, [dockAt, dockAt + DOCK_DUR], [0, 1], {\n    ...CLAMP,\n    easing: EASE_DOCK,\n  });\n\n  const firstH = logo ? R_EM * LF : m.cap * LF;\n  const firstW = logo ? firstH * R_RATIO : m.adv[0] * LF;\n  const bugH = H * (portrait ? 0.04 : 0.08);\n  const bugScale = bugH / firstH;\n  const margin = Math.min(W, H) * 0.075;\n  const bugRight = W - margin;\n  const bugBottom = H - margin;\n\n  const startLeft = groupLeft;\n  const startBottom = baseY;\n  const endLeft = bugRight - firstW * bugScale;\n  const curLeft = startLeft + (endLeft - startLeft) * dock;\n  const curBottom = startBottom + (bugBottom - startBottom) * dock;\n  const curScale = 1 + (bugScale - 1) * dock;\n  const bugOpacity = 1 - 0.18 * dock;\n\n  const enter = (i: number) =>\n    interpolate(t, [ENTER_AT + i * 2, ENTER_AT + i * 2 + 18], [0, 1], {\n      ...CLAMP,\n      easing: EASE_OUT,\n    });\n\n  const tailFont = logo ? manrope.fontFamily : urbanist.fontFamily;\n  const tailWeight = logo ? 600 : 500;\n  const tailNodes = tailAdv.map((adv, k) => {\n    const prevRight =\n      k === 0 ? groupLeft + firstAdv * LF : xs[k - 1] + tailAdv[k - 1] * LF;\n    const clipL = Math.max(0, prevRight - xs[k]);\n    const e = enter(k + 1);\n    const ch = logo ? tail[k] : letters[k + 1];\n    return (\n      <div\n        key={`${k}-${ch}`}\n        style={{\n          position: \"absolute\",\n          left: xs[k],\n          top: baseY - m.baseline * LF - LF * 0.15,\n          height: LF * 1.3,\n          width: adv * LF + LF * 0.3,\n          overflow: \"hidden\",\n          clipPath: `inset(0 0 0 ${clipL.toFixed(2)}px)`,\n          opacity: 1 - tuckP[k],\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            top: LF * 0.15,\n            fontFamily: tailFont,\n            fontWeight: tailWeight,\n            fontSize: LF,\n            lineHeight: `${LF}px`,\n            letterSpacing: `${LOGO_TRACK}em`,\n            whiteSpace: \"pre\",\n            color: c.ink,\n            transform: `translateY(${((1 - e) * LF * 0.9).toFixed(2)}px)`,\n          }}\n        >\n          {ch}\n        </div>\n      </div>\n    );\n  });\n\n  const firstEnter = enter(0);\n  const headFs = (portrait ? 132 : 124) * s;\n  const headLeft = portrait ? W * 0.08 : W * 0.075;\n  const headW = portrait ? W * 0.84 : W * 0.78;\n\n  return (\n    <AbsoluteFill style={{ background: stage, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          left: groupLeft,\n          top: baseY - firstH - LF * 0.2,\n          width: firstW + LF * 0.1,\n          height: firstH + LF * 0.2,\n          overflow: \"hidden\",\n          opacity: dock > 0 ? 0 : 1,\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            top: LF * 0.2,\n            transform: `translateY(${((1 - firstEnter) * firstH * 1.1).toFixed(2)}px)`,\n          }}\n        >\n          {logo ? (\n            <RMark height={firstH} color={c.ink} />\n          ) : (\n            <span\n              style={{\n                display: \"block\",\n                fontFamily: urbanist.fontFamily,\n                fontWeight: 500,\n                fontSize: LF,\n                lineHeight: `${firstH}px`,\n                color: c.ink,\n              }}\n            >\n              {head}\n            </span>\n          )}\n        </div>\n      </div>\n\n      {tailNodes}\n\n      {dock > 0 ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: curLeft,\n            top: curBottom - firstH * curScale,\n            width: firstW,\n            height: firstH,\n            transformOrigin: \"0 0\",\n            transform: `scale(${curScale.toFixed(4)})`,\n            opacity: bugOpacity,\n          }}\n        >\n          {logo ? (\n            <RMark height={firstH} color={c.ink} />\n          ) : (\n            <span\n              style={{\n                display: \"block\",\n                fontFamily: urbanist.fontFamily,\n                fontWeight: 500,\n                fontSize: LF,\n                lineHeight: `${firstH}px`,\n                color: c.ink,\n              }}\n            >\n              {head}\n            </span>\n          )}\n        </div>\n      ) : null}\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: headLeft,\n          width: headW,\n          top: portrait ? H * 0.36 : H * 0.27,\n          fontFamily: urbanist.fontFamily,\n          fontSize: headFs,\n          fontVariationSettings: '\"wght\" 400',\n          lineHeight: 1.04,\n          letterSpacing: \"-0.03em\",\n          color: c.ink,\n          textWrap: \"balance\",\n        }}\n      >\n        {words.map((w, i) => {\n          const at = headAt + i * 2.5;\n          const p = interpolate(t, [at, at + 16], [0, 1], {\n            ...CLAMP,\n            easing: EASE_OUT,\n          });\n          const hi = hiWords.has(\n            w.toLowerCase().replace(/[^\\p{L}\\p{N}]/gu, \"\"),\n          );\n          return (\n            <span\n              key={`${i}-${w}`}\n              style={{\n                display: \"inline-block\",\n                overflow: \"hidden\",\n                verticalAlign: \"top\",\n                paddingBottom: \"0.12em\",\n                marginBottom: \"-0.12em\",\n                marginRight: \"0.24em\",\n              }}\n            >\n              <span\n                style={{\n                  display: \"inline-block\",\n                  color: hi ? blue : c.ink,\n                  transform: `translateY(${((1 - p) * 1.1).toFixed(3)}em)`,\n                }}\n              >\n                {w}\n              </span>\n            </span>\n          );\n        })}\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/bug-dock.tsx"
    }
  ],
  "type": "registry:component"
}
