{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dot-text-hero",
  "type": "registry:ui",
  "title": "Dot Text Hero",
  "author": "Rishabh Bose <https://theb0se.vercel.app/>",
  "description": "Interactive headline hero featuring HTML5 canvas text rasterization into hexagonal-packed dots with cursor repulsion, spring-back physics, and twinkling.",
  "dependencies": [
    "framer-motion",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/ui/dot-text-hero.tsx",
      "content": "\"use client\";\n\nimport React, { useEffect, useRef } from \"react\";\nimport { motion, useReducedMotion } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface DotTextCanvasProps\n  extends React.HTMLAttributes<HTMLDivElement> {\n  /** Headline lines drawn as a field of dots. Default: [\"Cosign\", \"Awards\"] */\n  lines?: string[];\n  /** Dot colors. Each dot picks one at random and re-picks as it twinkles. */\n  colors?: string[];\n  /** Max dot diameter in CSS px. Default: 14 */\n  dotSize?: number;\n  /** Mouse repulsion radius in pixels. Default: 80 */\n  repelRadius?: number;\n  /** Mouse repulsion strength multiplier. Default: 5 */\n  repelStrength?: number;\n  /** Spring return stiffness factor. Default: 0.08 */\n  springStiffness?: number;\n  /** Velocity friction damping. Default: 0.82 */\n  damping?: number;\n  /** Twinkle interval in ms. Default: 80 */\n  twinkleInterval?: number;\n  /** Custom canvas font family */\n  fontFamily?: string;\n  className?: string;\n}\n\nexport interface DotTextHeroNavLink {\n  label: string;\n  href: string;\n}\n\nexport interface DotTextHeroCta {\n  label: string;\n  href?: string;\n  onClick?: () => void;\n}\n\nexport interface DotTextHeroProps\n  extends Omit<React.HTMLAttributes<HTMLElement>, \"title\"> {\n  /** Headline lines drawn as a field of dots */\n  lines?: string[];\n  /** Dot colors palette. Each dot picks one at random and re-picks as it twinkles. */\n  colors?: string[];\n  /** Max dot diameter in CSS px */\n  dotSize?: number;\n  /** Mouse repulsion radius in pixels. Default: 80 */\n  repelRadius?: number;\n  /** Mouse repulsion force multiplier. Default: 5 */\n  repelStrength?: number;\n  /** Spring return stiffness. Default: 0.08 */\n  springStiffness?: number;\n  /** Velocity damping coefficient. Default: 0.82 */\n  damping?: number;\n  /** Twinkle interval in ms. Default: 80 */\n  twinkleInterval?: number;\n  /** Custom canvas font family */\n  fontFamily?: string;\n  /** Top bar brand title. Default: \"cosign\" */\n  brandName?: string;\n  /** Top bar brand subtitle or edition. Default: \"Awards 2026\" */\n  brandTag?: string;\n  /** Navigation links for the top bar */\n  navLinks?: DotTextHeroNavLink[];\n  /** Top bar action button */\n  headerCta?: DotTextHeroCta;\n  /** Status badge text or node. Default: \"Cosigning is open\" */\n  badgeText?: React.ReactNode;\n  /** Whether to render glowing ping indicator dot on badge. Default: true */\n  showBadgePing?: boolean;\n  /** Hero subtitle / description below headline */\n  description?: React.ReactNode;\n  /** Primary call-to-action button */\n  primaryCta?: DotTextHeroCta;\n  /** Secondary call-to-action button */\n  secondaryCta?: DotTextHeroCta;\n  /** Whether to render the header navigation. Default: true */\n  showNav?: boolean;\n  className?: string;\n}\n\ninterface Dot {\n  hx: number;\n  hy: number;\n  x: number;\n  y: number;\n  vx: number;\n  vy: number;\n  color: number;\n  delay: number;\n  pop: number;\n}\n\nexport const DEFAULT_DOT_COLORS = [\n  \"#ffffff\",\n  \"#e5e5e5\",\n  \"#bdbdbd\",\n  \"#8a8a8a\",\n  \"#5c5c5c\",\n];\n\nconst DEFAULT_NAV_LINKS: DotTextHeroNavLink[] = [\n  { label: \"Awards\", href: \"#awards\" },\n  { label: \"Judges\", href: \"#judges\" },\n];\n\n/**\n * DotTextCanvas — Standalone interactive canvas that rasterizes text lines into a\n * hexagonal-packed dot grid with physics-driven cursor repulsion, spring-back, and twinkling.\n */\nexport function DotTextCanvas({\n  lines = [\"Cosign\", \"Awards\"],\n  colors = DEFAULT_DOT_COLORS,\n  dotSize = 14,\n  repelRadius = 80,\n  repelStrength = 5,\n  springStiffness = 0.08,\n  damping = 0.82,\n  twinkleInterval = 80,\n  fontFamily = \"ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif\",\n  className,\n  ...props\n}: DotTextCanvasProps) {\n  const reduceMotion = useReducedMotion();\n  const wrapRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  const linesKey = lines.join(\"|\");\n  const colorsKey = colors.join(\"|\");\n\n  useEffect(() => {\n    const wrap = wrapRef.current;\n    const canvas = canvasRef.current;\n    if (!wrap || !canvas) return;\n\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    let raf = 0;\n    let cancelled = false;\n    let visible = true;\n    let dots: Dot[] = [];\n    let w = 0;\n    let h = 0;\n    let radius = 6;\n    let start = performance.now();\n    let lastSwap = 0;\n    const dpr = typeof window !== \"undefined\" ? Math.min(window.devicePixelRatio || 1, 2) : 1;\n    const mouse = { x: -9999, y: -9999 };\n    const palette = colors && colors.length > 0 ? colors : DEFAULT_DOT_COLORS;\n    const validLines = lines && lines.length > 0 ? lines : [\"BoseUI\"];\n\n    const build = () => {\n      w = wrap.clientWidth;\n      if (!w) return;\n\n      // 1. Measure text so headline fills ~84% of wrap width\n      const probe = document.createElement(\"canvas\").getContext(\"2d\");\n      if (!probe) return;\n\n      probe.font = `800 100px ${fontFamily}`;\n      const widest = Math.max(\n        ...validLines.map((l) => probe.measureText(l).width || 100),\n        1\n      );\n      const fs = Math.min((100 * w * 0.84) / widest, 300);\n      const lineH = fs * 0.98;\n      h = Math.ceil(validLines.length * lineH + fs * 0.12);\n\n      if (w <= 0 || h <= 0) return;\n\n      canvas.width = Math.floor(w * dpr);\n      canvas.height = Math.floor(h * dpr);\n      canvas.style.width = `${w}px`;\n      canvas.style.height = `${h}px`;\n      wrap.style.height = `${h}px`;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n      // 2. Rasterise offscreen text\n      const off = document.createElement(\"canvas\");\n      off.width = Math.floor(w);\n      off.height = Math.floor(h);\n      const og = off.getContext(\"2d\", { willReadFrequently: true });\n      if (!og) return;\n\n      og.fillStyle = \"#fff\";\n      og.font = `800 ${fs}px ${fontFamily}`;\n      og.textAlign = \"center\";\n      og.textBaseline = \"alphabetic\";\n      validLines.forEach((l, i) => {\n        og.fillText(l, w / 2, lineH * i + fs * 0.8);\n      });\n\n      const data = og.getImageData(0, 0, off.width, off.height).data;\n\n      // 3. Dot size\n      const d = Math.max(6, Math.min(dotSize, w / 62));\n      radius = d / 2;\n\n      // 4. Hex-pack dots wherever text is solid (alpha > 150)\n      const step = d * 1.04;\n      const rowH = step * 0.866;\n      dots = [];\n      let row = 0;\n\n      for (let y = radius; y < h - radius; y += rowH, row++) {\n        const offset = row % 2 ? step / 2 : 0;\n        for (let x = radius + offset; x < w - radius; x += step) {\n          const idx = (Math.floor(y) * off.width + Math.floor(x)) * 4 + 3;\n          const a = data[idx];\n          if (a > 150) {\n            dots.push({\n              hx: x,\n              hy: y,\n              x,\n              y,\n              vx: 0,\n              vy: 0,\n              color: Math.floor(Math.random() * palette.length),\n              delay: (x / w) * 0.7 + Math.random() * 0.4,\n              pop: 0,\n            });\n          }\n        }\n      }\n      start = performance.now();\n    };\n\n    const tick = (t: number) => {\n      raf = requestAnimationFrame(tick);\n      if (!visible || !dots.length) return;\n      ctx.clearRect(0, 0, w, h);\n\n      // Twinkle: randomly re-color a small batch of dots so field stays lively\n      if (!reduceMotion && twinkleInterval > 0 && t - lastSwap > twinkleInterval) {\n        lastSwap = t;\n        const count = Math.min(7, Math.max(1, Math.floor(dots.length * 0.015)));\n        for (let k = 0; k < count; k++) {\n          const dot = dots[(Math.random() * dots.length) | 0];\n          if (dot) {\n            dot.color = (Math.random() * palette.length) | 0;\n            dot.pop = 1;\n          }\n        }\n      }\n\n      const elapsed = (t - start) / 1000;\n\n      for (let i = 0; i < dots.length; i++) {\n        const dot = dots[i];\n        const age = reduceMotion ? 1 : elapsed - dot.delay;\n        if (age <= 0) continue;\n        const intro = Math.min(1, age / 0.5);\n        const eased = 1 - Math.pow(1 - intro, 3);\n\n        if (!reduceMotion) {\n          const dx = dot.x - mouse.x;\n          const dy = dot.y - mouse.y;\n          const dist = Math.hypot(dx, dy);\n          if (dist < repelRadius && dist > 0.01) {\n            const f = Math.pow(1 - dist / repelRadius, 2) * repelStrength;\n            dot.vx += (dx / dist) * f;\n            dot.vy += (dy / dist) * f;\n          }\n          dot.vx += (dot.hx - dot.x) * springStiffness;\n          dot.vy += (dot.hy - dot.y) * springStiffness;\n          dot.vx *= damping;\n          dot.vy *= damping;\n          dot.x += dot.vx;\n          dot.y += dot.vy;\n          dot.pop *= 0.88;\n        }\n\n        const r = radius * eased * (1 + dot.pop * 0.35);\n        ctx.fillStyle = palette[dot.color] || \"#ffffff\";\n        ctx.beginPath();\n        ctx.arc(dot.x, dot.y, Math.max(0.1, r), 0, Math.PI * 2);\n        ctx.fill();\n      }\n    };\n\n    const onMove = (e: PointerEvent) => {\n      const rect = canvas.getBoundingClientRect();\n      mouse.x = e.clientX - rect.left;\n      mouse.y = e.clientY - rect.top;\n    };\n\n    const onLeave = () => {\n      mouse.x = -9999;\n      mouse.y = -9999;\n    };\n\n    const ro = new ResizeObserver(build);\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        visible = entry.isIntersecting;\n      },\n      { threshold: 0 }\n    );\n\n    (async () => {\n      if (typeof document !== \"undefined\" && document.fonts?.ready) {\n        await document.fonts.ready;\n      }\n      if (cancelled) return;\n      build();\n      ro.observe(wrap);\n      io.observe(wrap);\n      raf = requestAnimationFrame(tick);\n    })();\n\n    wrap.addEventListener(\"pointermove\", onMove);\n    wrap.addEventListener(\"pointerleave\", onLeave);\n    wrap.addEventListener(\"pointercancel\", onLeave);\n\n    return () => {\n      cancelled = true;\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      wrap.removeEventListener(\"pointermove\", onMove);\n      wrap.removeEventListener(\"pointerleave\", onLeave);\n      wrap.removeEventListener(\"pointercancel\", onLeave);\n    };\n  }, [\n    linesKey,\n    colorsKey,\n    dotSize,\n    repelRadius,\n    repelStrength,\n    springStiffness,\n    damping,\n    twinkleInterval,\n    fontFamily,\n    reduceMotion,\n  ]);\n\n  return (\n    <div\n      ref={wrapRef}\n      className={cn(\"relative w-full select-none touch-none\", className)}\n      aria-hidden=\"true\"\n      {...props}\n    >\n      <canvas ref={canvasRef} className=\"block w-full\" />\n    </div>\n  );\n}\n\n/**\n * DotTextHero — Production-grade hero section featuring the interactive canvas dot-matrix\n * headline with Hooke's Law spring physics, responsive navigation, status badge, and CTAs.\n */\nexport function DotTextHero({\n  lines = [\"BOSE\", \"UI\"],\n  colors = DEFAULT_DOT_COLORS,\n  dotSize = 4,\n  repelRadius = 20,\n  repelStrength = 9,\n  springStiffness = 0.08,\n  damping = 0.82,\n  twinkleInterval = 80,\n  fontFamily,\n  className,\n  ...props\n}: DotTextHeroProps) {\n\n\n\n\n  return (\n    <section\n      className={cn(\n        \"relative flex min-h-[640px] w-full flex-col justify-between overflow-hidden bg-[#080808] text-white\",\n        className\n      )}\n      {...props}\n    >\n\n\n      {/* Hero Body */}\n      <div className=\"relative z-10 mx-auto flex w-full max-w-5xl flex-1 flex-col items-center justify-center px-4 py-12 sm:py-16\">\n\n\n        {/* Canvas Dot Headline */}\n        <DotTextCanvas\n          lines={lines}\n          colors={colors}\n          dotSize={dotSize}\n          repelRadius={repelRadius}\n          repelStrength={repelStrength}\n          springStiffness={springStiffness}\n          damping={damping}\n          twinkleInterval={twinkleInterval}\n          fontFamily={fontFamily}\n        />\n\n\n\n      </div>\n    </section>\n  );\n}\n\nexport default DotTextHero;\n",
      "type": "registry:ui",
      "target": "components/ui/dot-text-hero.tsx"
    },
    {
      "path": "lib/utils.ts",
      "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport async function copyToClipboard(text: string): Promise<boolean> {\n  if (typeof window === \"undefined\") return false;\n\n  try {\n    if (navigator.clipboard && window.isSecureContext) {\n      await navigator.clipboard.writeText(text);\n      return true;\n    }\n\n    const textArea = document.createElement(\"textarea\");\n    textArea.value = text;\n    textArea.style.position = \"fixed\";\n    textArea.style.left = \"-999999px\";\n    textArea.style.top = \"-999999px\";\n    document.body.appendChild(textArea);\n    textArea.focus();\n    textArea.select();\n    const succeeded = document.execCommand(\"copy\");\n    textArea.remove();\n    return succeeded;\n  } catch {\n    return false;\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/utils.ts"
    }
  ]
}
