{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "butterflies",
  "title": "Butterflies",
  "description": "Realistic butterflies with painted, flapping wings that flutter around a block and circle your cursor, in six colorful species.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/clover-ui/butterflies.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type ButterflySpecies =\n  \"monarch\" | \"morpho\" | \"swallowtail\" | \"emerald\" | \"violet\" | \"glasswing\";\nexport type ButterfliesSpecies = ButterflySpecies | \"mixed\";\n\nexport interface ButterfliesProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n  species?: ButterfliesSpecies;\n  count?: number;\n  size?: number;\n  speed?: number;\n  followCursor?: boolean;\n  animate?: boolean;\n}\n\ntype Pt = [number, number];\ntype Seg = [Pt, Pt, Pt];\ntype Shape = { path: Path2D; rim: Path2D; pts: Pt[]; edge: Pt[]; center: Pt };\ntype Part = \"fore\" | \"hind\";\ntype Paint = (ctx: CanvasRenderingContext2D, part: Part) => void;\n\ntype Look = {\n  fore: Shape;\n  hind: Shape;\n  base: string | ((ctx: CanvasRenderingContext2D, part: Part) => CanvasGradient);\n  vein: string;\n  veinWidth: number;\n  margin: string;\n  marginWidth: Record<Part, number>;\n  apex?: number;\n  dots?: string;\n  under?: Paint;\n  over?: Paint;\n  bodyDots?: string;\n};\n\ntype Sprites = {\n  wing: HTMLCanvasElement;\n  dark: HTMLCanvasElement;\n  shadow: HTMLCanvasElement;\n};\n\ntype Flyer = {\n  species: ButterflySpecies;\n  scale: number;\n  seed: number;\n  x: number;\n  y: number;\n  heading: number;\n  phase: number;\n  glide: number;\n};\n\ntype Live = {\n  species: ButterfliesSpecies;\n  count: number;\n  size: number;\n  speed: number;\n  followCursor: boolean;\n  animate: boolean;\n};\n\nconst SPECIES: ButterflySpecies[] = [\n  \"monarch\",\n  \"morpho\",\n  \"swallowtail\",\n  \"emerald\",\n  \"violet\",\n  \"glasswing\",\n];\nconst ROOT: Pt = [0.05, -0.04];\nconst BOX = { x: -0.06, y: -0.92, w: 1.18, h: 2.04 };\nconst BODY_BOX = { x: -0.26, y: -0.86, w: 0.52, h: 1.38 };\n\nfunction bezier(a: Pt, b: Pt, c: Pt, d: Pt, t: number): Pt {\n  const m = 1 - t;\n  return [\n    m * m * m * a[0] + 3 * m * m * t * b[0] + 3 * m * t * t * c[0] + t * t * t * d[0],\n    m * m * m * a[1] + 3 * m * m * t * b[1] + 3 * m * t * t * c[1] + t * t * t * d[1],\n  ];\n}\n\nfunction shape(start: Pt, segs: Seg[], edge: number[]): Shape {\n  const pts: Pt[] = [start];\n  const edgePts: Pt[] = [];\n  let from = start;\n  segs.forEach((seg, index) => {\n    for (let i = 1; i <= 18; i++) {\n      const p = bezier(from, seg[0], seg[1], seg[2], i / 18);\n      pts.push(p);\n      if (edge.includes(index)) edgePts.push(p);\n    }\n    from = seg[2];\n  });\n  const path = new Path2D();\n  pts.forEach(([x, y], i) => (i ? path.lineTo(x, y) : path.moveTo(x, y)));\n  path.closePath();\n  const rim = new Path2D();\n  edgePts.forEach(([x, y], i) => (i ? rim.lineTo(x, y) : rim.moveTo(x, y)));\n  const center: Pt = [\n    pts.reduce((sum, p) => sum + p[0], 0) / pts.length,\n    pts.reduce((sum, p) => sum + p[1], 0) / pts.length,\n  ];\n  return { path, rim, pts, edge: edgePts, center };\n}\n\nfunction shapes() {\n  const fore = shape(\n    [0.04, -0.1],\n    [\n      [\n        [0.22, -0.48],\n        [0.62, -0.8],\n        [0.96, -0.7],\n      ],\n      [\n        [1.04, -0.52],\n        [0.9, -0.24],\n        [0.66, -0.08],\n      ],\n      [\n        [0.45, 0],\n        [0.2, 0.02],\n        [0.05, -0.01],\n      ],\n    ],\n    [0, 1],\n  );\n  const pointed = shape(\n    [0.04, -0.1],\n    [\n      [\n        [0.24, -0.5],\n        [0.66, -0.8],\n        [1, -0.66],\n      ],\n      [\n        [0.92, -0.46],\n        [0.84, -0.22],\n        [0.62, -0.07],\n      ],\n      [\n        [0.42, 0],\n        [0.2, 0.02],\n        [0.05, -0.01],\n      ],\n    ],\n    [0, 1],\n  );\n  const hind = shape(\n    ROOT,\n    [\n      [\n        [0.4, -0.1],\n        [0.78, 0.02],\n        [0.8, 0.3],\n      ],\n      [\n        [0.82, 0.55],\n        [0.5, 0.72],\n        [0.28, 0.62],\n      ],\n      [[0.12, 0.52], [0.04, 0.24], ROOT],\n    ],\n    [0, 1],\n  );\n  const tailed = shape(\n    ROOT,\n    [\n      [\n        [0.42, -0.1],\n        [0.8, 0.02],\n        [0.78, 0.3],\n      ],\n      [\n        [0.74, 0.46],\n        [0.52, 0.58],\n        [0.42, 0.62],\n      ],\n      [\n        [0.4, 0.8],\n        [0.36, 0.98],\n        [0.3, 1.02],\n      ],\n      [\n        [0.24, 0.98],\n        [0.28, 0.78],\n        [0.24, 0.64],\n      ],\n      [[0.12, 0.55], [0.04, 0.26], ROOT],\n    ],\n    [0, 1, 2],\n  );\n  return { fore, pointed, hind, tailed };\n}\n\nfunction spot(ctx: CanvasRenderingContext2D, x: number, y: number, r: number, color: string) {\n  ctx.fillStyle = color;\n  ctx.beginPath();\n  ctx.arc(x, y, r, 0, Math.PI * 2);\n  ctx.fill();\n}\n\nfunction band(\n  ctx: CanvasRenderingContext2D,\n  from: Pt,\n  via: Pt,\n  to: Pt,\n  width: number,\n  color: string | CanvasGradient,\n) {\n  ctx.strokeStyle = color;\n  ctx.lineWidth = width;\n  ctx.lineCap = \"round\";\n  ctx.beginPath();\n  ctx.moveTo(from[0], from[1]);\n  ctx.quadraticCurveTo(via[0], via[1], to[0], to[1]);\n  ctx.stroke();\n}\n\nfunction sheen(ctx: CanvasRenderingContext2D, at: Pt, radius: number, color: string) {\n  const g = ctx.createRadialGradient(at[0], at[1], 0, at[0], at[1], radius);\n  g.addColorStop(0, color);\n  g.addColorStop(1, \"rgba(0, 0, 0, 0)\");\n  ctx.fillStyle = g;\n  ctx.fillRect(BOX.x, BOX.y, BOX.w, BOX.h);\n}\n\nfunction looks(): Record<ButterflySpecies, Look> {\n  const { fore, pointed, hind, tailed } = shapes();\n  return {\n    monarch: {\n      fore,\n      hind,\n      base: (ctx) => {\n        const g = ctx.createLinearGradient(0.05, 0, 0.9, -0.3);\n        g.addColorStop(0, \"#b8470a\");\n        g.addColorStop(0.45, \"#e8741a\");\n        g.addColorStop(1, \"#f59a32\");\n        return g;\n      },\n      vein: \"#16100c\",\n      veinWidth: 0.026,\n      margin: \"#16100c\",\n      marginWidth: { fore: 0.17, hind: 0.15 },\n      apex: 0.3,\n      dots: \"#fdf6e8\",\n      over: (ctx, part) => {\n        if (part !== \"fore\") return;\n        spot(ctx, 0.79, -0.6, 0.032, \"#f6a640\");\n        spot(ctx, 0.86, -0.51, 0.028, \"#f6a640\");\n        spot(ctx, 0.72, -0.67, 0.024, \"#fdf6e8\");\n        spot(ctx, 0.9, -0.62, 0.02, \"#fdf6e8\");\n      },\n      bodyDots: \"#fdf6e8\",\n    },\n    morpho: {\n      fore,\n      hind,\n      base: (ctx) => {\n        const g = ctx.createRadialGradient(0.3, -0.18, 0, 0.3, -0.18, 0.95);\n        g.addColorStop(0, \"#9be7ff\");\n        g.addColorStop(0.3, \"#2f95ff\");\n        g.addColorStop(0.7, \"#0b45c9\");\n        g.addColorStop(1, \"#06215f\");\n        return g;\n      },\n      vein: \"rgba(4, 22, 74, 0.45)\",\n      veinWidth: 0.012,\n      margin: \"#0a0c14\",\n      marginWidth: { fore: 0.22, hind: 0.16 },\n      apex: 0.27,\n      dots: \"#e8f4ff\",\n      over: (ctx) => {\n        const g = ctx.createRadialGradient(0.32, -0.26, 0, 0.32, -0.26, 0.5);\n        g.addColorStop(0, \"rgba(210, 245, 255, 0.4)\");\n        g.addColorStop(1, \"rgba(210, 245, 255, 0)\");\n        ctx.fillStyle = g;\n        ctx.fillRect(BOX.x, BOX.y, BOX.w, BOX.h);\n      },\n    },\n    swallowtail: {\n      fore: pointed,\n      hind: tailed,\n      base: (ctx) => {\n        const g = ctx.createLinearGradient(0.05, 0, 0.9, -0.2);\n        g.addColorStop(0, \"#f2bf2c\");\n        g.addColorStop(1, \"#ffe684\");\n        return g;\n      },\n      vein: \"#1a1408\",\n      veinWidth: 0.014,\n      margin: \"#14110a\",\n      marginWidth: { fore: 0.2, hind: 0.24 },\n      dots: \"#ffe27a\",\n      under: (ctx, part) => {\n        ctx.strokeStyle = \"#14110a\";\n        ctx.lineCap = \"round\";\n        const stripes: [Pt, Pt, number][] =\n          part === \"fore\"\n            ? [\n                [[0.2, -0.58], [0.1, -0.04], 0.08],\n                [[0.42, -0.74], [0.27, -0.03], 0.075],\n                [[0.62, -0.78], [0.47, -0.06], 0.05],\n                [[0.8, -0.74], [0.7, -0.4], 0.035],\n              ]\n            : [[[0.04, -0.02], [0.24, 0.64], 0.13]];\n        for (const [a, b, width] of stripes) {\n          ctx.lineWidth = width;\n          ctx.beginPath();\n          ctx.moveTo(a[0], a[1]);\n          ctx.lineTo(b[0], b[1]);\n          ctx.stroke();\n        }\n      },\n      over: (ctx, part) => {\n        if (part !== \"hind\") return;\n        for (const [x, y] of [\n          [0.36, 0.5],\n          [0.48, 0.47],\n          [0.6, 0.4],\n        ] as Pt[])\n          spot(ctx, x, y, 0.026, \"#4c8dff\");\n        spot(ctx, 0.3, 0.58, 0.04, \"#f47a1f\");\n      },\n    },\n    emerald: {\n      fore: pointed,\n      hind: tailed,\n      base: (ctx) => {\n        const g = ctx.createLinearGradient(0.05, 0, 0.9, -0.3);\n        g.addColorStop(0, \"#0d2a20\");\n        g.addColorStop(1, \"#06140f\");\n        return g;\n      },\n      vein: \"rgba(0, 0, 0, 0.4)\",\n      veinWidth: 0.012,\n      margin: \"#050b08\",\n      marginWidth: { fore: 0.1, hind: 0.12 },\n      dots: \"#1fbf86\",\n      under: (ctx, part) => {\n        const g = ctx.createLinearGradient(0.1, 0.3, 0.8, -0.6);\n        g.addColorStop(0, \"#14d68f\");\n        g.addColorStop(0.5, \"#5cffc4\");\n        g.addColorStop(1, \"#1bc98a\");\n        if (part === \"fore\") band(ctx, [0.3, -0.1], [0.46, -0.4], [0.72, -0.66], 0.2, g);\n        else band(ctx, [0.12, 0.1], [0.36, 0.1], [0.62, 0.22], 0.22, g);\n        sheen(ctx, part === \"fore\" ? [0.5, -0.4] : [0.36, 0.14], 0.4, \"rgba(150, 255, 220, 0.35)\");\n      },\n      over: (ctx, part) => {\n        if (part === \"hind\") spot(ctx, 0.32, 0.56, 0.035, \"#5fa8ff\");\n      },\n    },\n    violet: {\n      fore,\n      hind,\n      base: (ctx) => {\n        const g = ctx.createRadialGradient(0.3, -0.18, 0, 0.3, -0.18, 0.95);\n        g.addColorStop(0, \"#f0d4ff\");\n        g.addColorStop(0.3, \"#b46bff\");\n        g.addColorStop(0.7, \"#7a2fe0\");\n        g.addColorStop(1, \"#3e1478\");\n        return g;\n      },\n      vein: \"rgba(40, 8, 80, 0.45)\",\n      veinWidth: 0.012,\n      margin: \"#1a0b2e\",\n      marginWidth: { fore: 0.16, hind: 0.14 },\n      apex: 0.22,\n      dots: \"#f6e8ff\",\n      over: (ctx, part) => {\n        sheen(ctx, part === \"fore\" ? [0.34, -0.24] : [0.32, 0.18], 0.5, \"rgba(255, 210, 255, 0.4)\");\n        if (part === \"hind\") {\n          spot(ctx, 0.58, 0.4, 0.045, \"#ff8ad8\");\n          spot(ctx, 0.58, 0.4, 0.022, \"#1a0b2e\");\n        }\n      },\n    },\n    glasswing: {\n      fore: pointed,\n      hind,\n      base: \"rgba(255, 255, 255, 0.07)\",\n      vein: \"rgba(70, 35, 22, 0.6)\",\n      veinWidth: 0.012,\n      margin: \"#3a1f14\",\n      marginWidth: { fore: 0.12, hind: 0.1 },\n      over: (ctx, part) => {\n        sheen(ctx, part === \"fore\" ? [0.4, -0.3] : [0.35, 0.2], 0.45, \"rgba(255, 255, 255, 0.16)\");\n        if (part === \"fore\") band(ctx, [0.66, -0.7], [0.74, -0.64], [0.82, -0.52], 0.05, \"#fbf6ee\");\n      },\n      bodyDots: \"#e9dccb\",\n    },\n  };\n}\n\nfunction veins(ctx: CanvasRenderingContext2D, s: Shape, look: Look) {\n  ctx.strokeStyle = look.vein;\n  ctx.lineWidth = look.veinWidth;\n  ctx.lineCap = \"round\";\n  const n = 7;\n  for (let i = 0; i < n; i++) {\n    const [ex, ey] = s.edge[Math.round((i / (n - 1)) * (s.edge.length - 1))];\n    const mx = (ROOT[0] + ex) / 2;\n    const my = (ROOT[1] + ey) / 2;\n    const dx = ex - ROOT[0];\n    const dy = ey - ROOT[1];\n    ctx.beginPath();\n    ctx.moveTo(ROOT[0], ROOT[1]);\n    ctx.quadraticCurveTo(mx - dy * 0.08, my + dx * 0.08, ex, ey);\n    ctx.stroke();\n  }\n}\n\nfunction paintWing(ctx: CanvasRenderingContext2D, part: Part, look: Look) {\n  const s = look[part];\n  ctx.save();\n  ctx.clip(s.path);\n  ctx.fillStyle = typeof look.base === \"function\" ? look.base(ctx, part) : look.base;\n  ctx.fillRect(BOX.x, BOX.y, BOX.w, BOX.h);\n  look.under?.(ctx, part);\n  veins(ctx, s, look);\n  if (look.apex && part === \"fore\") spot(ctx, 1.02, -0.74, look.apex, look.margin);\n  ctx.strokeStyle = look.margin;\n  ctx.lineWidth = look.marginWidth[part];\n  ctx.lineJoin = \"round\";\n  ctx.lineCap = \"butt\";\n  ctx.stroke(s.rim);\n  const inset = look.marginWidth[part] * 0.24;\n  const step = Math.max(1, Math.floor(s.edge.length / 12));\n  for (let i = step; look.dots && i < s.edge.length - 1; i += step) {\n    const [x, y] = s.edge[i];\n    const dx = s.center[0] - x;\n    const dy = s.center[1] - y;\n    const d = Math.hypot(dx, dy) || 1;\n    spot(ctx, x + (dx / d) * inset, y + (dy / d) * inset, 0.013, look.dots);\n  }\n  look.over?.(ctx, part);\n  ctx.restore();\n  ctx.strokeStyle = \"rgba(0, 0, 0, 0.35)\";\n  ctx.lineWidth = 0.008;\n  ctx.stroke(s.path);\n}\n\nfunction unitCanvas(px: number, box: typeof BOX) {\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = Math.max(1, Math.ceil(box.w * px));\n  canvas.height = Math.max(1, Math.ceil(box.h * px));\n  const ctx = canvas.getContext(\"2d\");\n  ctx?.setTransform(px, 0, 0, px, -box.x * px, -box.y * px);\n  return { canvas, ctx };\n}\n\nfunction wingSprites(look: Look, px: number): Sprites | null {\n  const { canvas: wing, ctx } = unitCanvas(px, BOX);\n  const { canvas: dark, ctx: darkCtx } = unitCanvas(px, BOX);\n  const { canvas: shadow, ctx: shadowCtx } = unitCanvas(px, BOX);\n  if (!ctx || !darkCtx || !shadowCtx) return null;\n  paintWing(ctx, \"hind\", look);\n  paintWing(ctx, \"fore\", look);\n  darkCtx.setTransform(1, 0, 0, 1, 0, 0);\n  darkCtx.drawImage(wing, 0, 0);\n  darkCtx.globalCompositeOperation = \"source-in\";\n  darkCtx.fillStyle = \"#05030a\";\n  darkCtx.fillRect(0, 0, dark.width, dark.height);\n  shadowCtx.setTransform(1, 0, 0, 1, 0, 0);\n  shadowCtx.shadowColor = \"#000\";\n  shadowCtx.shadowBlur = px * 0.05;\n  shadowCtx.shadowOffsetX = shadow.width;\n  shadowCtx.drawImage(dark, -shadow.width, 0);\n  return { wing, dark, shadow };\n}\n\nconst BODY = [\"#1c1611\", \"#120d0a\", \"#3a2e25\", \"#4a3c31\", \"#140f0b\", \"#17110d\"];\n\nfunction bodySprite(px: number, dots?: string) {\n  const { canvas, ctx } = unitCanvas(px, BODY_BOX);\n  if (!ctx) return canvas;\n  const [line, edge, mid, light, deep, head] = BODY;\n  ctx.strokeStyle = line;\n  ctx.lineWidth = 0.014;\n  ctx.lineCap = \"round\";\n  for (const side of [-1, 1]) {\n    ctx.beginPath();\n    ctx.moveTo(side * 0.02, -0.4);\n    ctx.quadraticCurveTo(side * 0.06, -0.62, side * 0.17, -0.78);\n    ctx.stroke();\n    spot(ctx, side * 0.175, -0.79, 0.024, line);\n  }\n  const abdomen = ctx.createLinearGradient(-0.05, 0, 0.05, 0);\n  abdomen.addColorStop(0, edge);\n  abdomen.addColorStop(0.5, mid);\n  abdomen.addColorStop(1, edge);\n  ctx.fillStyle = abdomen;\n  ctx.beginPath();\n  ctx.ellipse(0, 0.16, 0.042, 0.27, 0, 0, Math.PI * 2);\n  ctx.fill();\n  const thorax = ctx.createRadialGradient(-0.015, -0.2, 0, 0, -0.17, 0.09);\n  thorax.addColorStop(0, light);\n  thorax.addColorStop(1, deep);\n  ctx.fillStyle = thorax;\n  ctx.beginPath();\n  ctx.ellipse(0, -0.17, 0.062, 0.13, 0, 0, Math.PI * 2);\n  ctx.fill();\n  spot(ctx, 0, -0.35, 0.05, head);\n  if (dots) {\n    for (const [x, y] of [\n      [-0.03, -0.37],\n      [0.03, -0.37],\n      [-0.035, -0.22],\n      [0.035, -0.22],\n      [0, -0.12],\n    ] as Pt[])\n      spot(ctx, x, y, 0.011, dots);\n  }\n  return canvas;\n}\n\nfunction hash(n: number) {\n  const h = Math.sin(n * 127.1) * 43758.5453;\n  return h - Math.floor(h);\n}\n\nfunction noise(x: number) {\n  const i = Math.floor(x);\n  const f = x - i;\n  const u = f * f * (3 - 2 * f);\n  return hash(i) + (hash(i + 1) - hash(i)) * u;\n}\n\nconst wrap = (a: number) => Math.atan2(Math.sin(a), Math.cos(a));\n\nfunction Butterflies({\n  species = \"mixed\",\n  count = 12,\n  size = 56,\n  speed = 1,\n  followCursor = true,\n  animate = true,\n  className,\n  ref,\n  ...props\n}: ButterfliesProps) {\n  const hostRef = React.useRef<HTMLDivElement | null>(null);\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const syncRef = React.useRef<(() => void) | null>(null);\n  const live = React.useRef<Live>({\n    species,\n    count,\n    size,\n    speed,\n    followCursor,\n    animate,\n  });\n\n  const setHost = React.useCallback(\n    (node: HTMLDivElement | null) => {\n      hostRef.current = node;\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) ref.current = node;\n    },\n    [ref],\n  );\n\n  React.useEffect(() => {\n    live.current = { species, count, size, speed, followCursor, animate };\n    syncRef.current?.();\n  }, [species, count, size, speed, followCursor, animate]);\n\n  React.useEffect(() => {\n    const host = hostRef.current;\n    const canvas = canvasRef.current;\n    const ctx = canvas?.getContext(\"2d\");\n    if (!host || !canvas || !ctx) return;\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const art = looks();\n    let sprites: Partial<Record<ButterflySpecies, Sprites>> = {};\n    let bodies: Partial<Record<ButterflySpecies, HTMLCanvasElement>> = {};\n    let spriteKey = \"\";\n    let flyers: Flyer[] = [];\n    let flyerKey = \"\";\n    let raf = 0;\n    let visible = true;\n    let last = 0;\n    let time = 0;\n    let dpr = 1;\n    let pointer: Pt | null = null;\n    const resize = () => {\n      dpr = Math.min(window.devicePixelRatio || 1, 2);\n      const w = Math.max(1, Math.round(host.clientWidth * dpr));\n      const h = Math.max(1, Math.round(host.clientHeight * dpr));\n      if (canvas.width !== w || canvas.height !== h) {\n        canvas.width = w;\n        canvas.height = h;\n      }\n    };\n\n    const ensure = () => {\n      const L = live.current;\n      const px = Math.ceil((Math.max(8, L.size) / 2) * 1.2 * dpr);\n      const key = `${px}`;\n      if (key !== spriteKey) {\n        spriteKey = key;\n        sprites = {};\n        bodies = {};\n      }\n      const W = host.clientWidth;\n      const H = host.clientHeight;\n      const n = Math.max(0, Math.min(60, Math.round(L.count)));\n      const nextKey = `${L.species}:${n}`;\n      if (nextKey !== flyerKey) {\n        flyerKey = nextKey;\n        flyers = Array.from({ length: n }, (_, i) => {\n          const old = flyers[i];\n          const kind = L.species === \"mixed\" ? SPECIES[i % SPECIES.length] : L.species;\n          const seed = old?.seed ?? Math.random() * 1000;\n          return {\n            species: kind,\n            scale: old?.scale ?? 0.72 + hash(seed) * 0.42,\n            seed,\n            x: old?.x ?? W * (0.1 + Math.random() * 0.8),\n            y: old?.y ?? H * (0.1 + Math.random() * 0.8),\n            heading: old?.heading ?? Math.random() * Math.PI * 2,\n            phase: old?.phase ?? Math.random() * Math.PI * 2,\n            glide: old?.glide ?? 0,\n          };\n        }).sort((a, b) => a.scale - b.scale);\n      }\n      for (const f of flyers) {\n        if (!sprites[f.species]) {\n          const made = wingSprites(art[f.species], px);\n          if (made) sprites[f.species] = made;\n        }\n        if (!bodies[f.species]) bodies[f.species] = bodySprite(px, art[f.species].bodyDots);\n      }\n    };\n\n    const step = (dt: number) => {\n      const L = live.current;\n      const W = host.clientWidth;\n      const H = host.clientHeight;\n      const rect = host.getBoundingClientRect();\n      const target =\n        L.followCursor && pointer ? ([pointer[0] - rect.left, pointer[1] - rect.top] as Pt) : null;\n      const chasing =\n        target && target[0] >= 0 && target[1] >= 0 && target[0] <= W && target[1] <= H;\n      for (const f of flyers) {\n        const s = L.size * f.scale;\n        let turn = (noise(time * 0.45 + f.seed) - 0.5) * 5;\n        let pace = 1;\n        const margin = Math.min(W, H) * 0.12 + s * 0.5;\n        if (f.x < margin || f.x > W - margin || f.y < margin || f.y > H - margin) {\n          turn += wrap(Math.atan2(H / 2 - f.y, W / 2 - f.x) - f.heading) * 2.4;\n        }\n        if (chasing && target) {\n          const orbit = s * (0.9 + hash(f.seed + 3) * 1.4);\n          const tx = target[0] + Math.cos(time * 0.9 + f.seed) * orbit;\n          const ty = target[1] + Math.sin(time * 1.1 + f.seed) * orbit * 0.7;\n          const d = Math.hypot(tx - f.x, ty - f.y);\n          turn += wrap(Math.atan2(ty - f.y, tx - f.x) - f.heading) * 3.2;\n          pace = Math.min(1.6, 0.5 + d / (s * 3));\n        }\n        f.heading = wrap(f.heading + Math.max(-4, Math.min(4, turn)) * dt);\n        const glideWanted = noise(time * 0.3 + f.seed * 2) > 0.66 ? 1 : 0;\n        f.glide += (glideWanted - f.glide) * Math.min(1, dt * 3);\n        const v = s * (1.05 + noise(time * 0.6 + f.seed) * 0.5) * pace;\n        f.x += Math.cos(f.heading) * v * dt;\n        f.y += Math.sin(f.heading) * v * dt;\n        f.x = Math.max(-s, Math.min(W + s, f.x));\n        f.y = Math.max(-s, Math.min(H + s, f.y));\n        f.phase += dt * Math.PI * 2 * (7.5 - f.glide * 5.5);\n      }\n    };\n\n    const draw = (still: boolean) => {\n      const L = live.current;\n      ctx.setTransform(1, 0, 0, 1, 0, 0);\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\n      for (const f of flyers) {\n        const set = sprites[f.species];\n        const body = bodies[f.species];\n        if (!set || !body) continue;\n        const u = (L.size * f.scale) / 2;\n        const beat = 0.5 + 0.5 * Math.cos(f.phase);\n        const open = still ? 0.96 : 0.12 + 0.88 * (beat * (1 - f.glide) + 0.92 * f.glide);\n        const lift = still ? 0 : Math.sin(f.phase) * u * 0.08 * (1 - f.glide);\n        const angle = f.heading + Math.PI / 2;\n        const lean = still ? 0 : Math.sin(f.phase * 0.5 + f.seed) * 0.06;\n\n        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n        ctx.translate(f.x + u * 0.35, f.y + u * 0.55);\n        ctx.rotate(angle);\n        ctx.scale(u, u);\n        ctx.globalAlpha = 0.14;\n        for (const side of [1, -1]) {\n          ctx.save();\n          ctx.scale(side * open, 1);\n          ctx.drawImage(set.shadow, BOX.x, BOX.y, BOX.w, BOX.h);\n          ctx.restore();\n        }\n\n        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n        ctx.translate(f.x, f.y + lift);\n        ctx.rotate(angle + lean);\n        ctx.scale(u, u);\n        for (const side of [1, -1]) {\n          ctx.save();\n          ctx.scale(side * open, 1);\n          ctx.globalAlpha = 1;\n          ctx.drawImage(set.wing, BOX.x, BOX.y, BOX.w, BOX.h);\n          ctx.globalAlpha = (1 - open) * 0.45;\n          ctx.drawImage(set.dark, BOX.x, BOX.y, BOX.w, BOX.h);\n          ctx.restore();\n        }\n        ctx.globalAlpha = 1;\n        ctx.drawImage(body, BODY_BOX.x, BODY_BOX.y, BODY_BOX.w, BODY_BOX.h);\n      }\n      ctx.setTransform(1, 0, 0, 1, 0, 0);\n      ctx.globalAlpha = 1;\n    };\n\n    const tick = (now: number) => {\n      raf = 0;\n      const L = live.current;\n      const moving = L.animate && L.speed > 0 && !reduced.matches;\n      const dt = last ? Math.min((now - last) / 1000, 0.05) * L.speed : 0;\n      last = now;\n      ensure();\n      if (moving) {\n        time += dt;\n        step(dt);\n      }\n      draw(!moving);\n      if (moving && visible && !document.hidden) raf = requestAnimationFrame(tick);\n    };\n\n    const schedule = () => {\n      if (raf) return;\n      last = 0;\n      raf = requestAnimationFrame(tick);\n    };\n\n    syncRef.current = () => {\n      resize();\n      schedule();\n    };\n\n    resize();\n    schedule();\n\n    const ro = new ResizeObserver(() => syncRef.current?.());\n    ro.observe(host);\n\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      if (visible) schedule();\n    });\n    io.observe(host);\n\n    const onPointer = (event: PointerEvent) => {\n      pointer = [event.clientX, event.clientY];\n    };\n    const onLeave = (event: PointerEvent) => {\n      if (!event.relatedTarget) pointer = null;\n    };\n    const onVisibility = () => {\n      if (!document.hidden) schedule();\n    };\n    const onMotion = () => schedule();\n    window.addEventListener(\"pointermove\", onPointer, { passive: true });\n    document.addEventListener(\"pointerout\", onLeave);\n    document.addEventListener(\"visibilitychange\", onVisibility);\n    reduced.addEventListener(\"change\", onMotion);\n\n    return () => {\n      if (raf) cancelAnimationFrame(raf);\n      syncRef.current = null;\n      ro.disconnect();\n      io.disconnect();\n      window.removeEventListener(\"pointermove\", onPointer);\n      document.removeEventListener(\"pointerout\", onLeave);\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      reduced.removeEventListener(\"change\", onMotion);\n    };\n  }, []);\n\n  return (\n    <div\n      ref={setHost}\n      aria-hidden=\"true\"\n      className={cn(\"pointer-events-none absolute inset-0 overflow-hidden\", className)}\n      {...props}\n    >\n      <canvas ref={canvasRef} className=\"block h-full w-full\" />\n    </div>\n  );\n}\n\nButterflies.displayName = \"Butterflies\";\n\nexport { Butterflies };\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "addedAt": "2026-10-20"
  },
  "type": "registry:component"
}