{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carrier-bird",
  "type": "registry:component",
  "title": "Carrier Bird",
  "description": "A round little bird hops beside a sealed letter; press send and it hops over, grabs the envelope and flaps off on a dotted trail before a tick confirms it's sent.",
  "categories": [
    "illustration",
    "Characters"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json"
  ],
  "files": [
    {
      "path": "registry/devl/carrier-bird.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { RotateCcwIcon, SendIcon } from \"lucide-react\";\nimport {\n  type AnimationPlaybackControls,\n  animate,\n  motion,\n  useInView,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport type CarrierBirdHue = \"sky\" | \"teal\" | \"emerald\" | \"violet\" | \"indigo\" | \"rose\" | \"amber\" | \"orange\";\n\nconst hues: Record<CarrierBirdHue, string> = {\n  sky: \"[--hue:var(--color-sky-400)] [--hue-deep:var(--color-sky-600)] [--soft:var(--color-sky-100)] [--ink:var(--color-sky-950)]\",\n  teal: \"[--hue:var(--color-teal-400)] [--hue-deep:var(--color-teal-600)] [--soft:var(--color-teal-100)] [--ink:var(--color-teal-950)]\",\n  emerald:\n    \"[--hue:var(--color-emerald-400)] [--hue-deep:var(--color-emerald-600)] [--soft:var(--color-emerald-100)] [--ink:var(--color-emerald-950)]\",\n  violet:\n    \"[--hue:var(--color-violet-400)] [--hue-deep:var(--color-violet-600)] [--soft:var(--color-violet-100)] [--ink:var(--color-violet-950)]\",\n  indigo:\n    \"[--hue:var(--color-indigo-400)] [--hue-deep:var(--color-indigo-600)] [--soft:var(--color-indigo-100)] [--ink:var(--color-indigo-950)]\",\n  rose: \"[--hue:var(--color-rose-400)] [--hue-deep:var(--color-rose-600)] [--soft:var(--color-rose-100)] [--ink:var(--color-rose-950)]\",\n  amber:\n    \"[--hue:var(--color-amber-400)] [--hue-deep:var(--color-amber-600)] [--soft:var(--color-amber-100)] [--ink:var(--color-amber-950)]\",\n  orange:\n    \"[--hue:var(--color-orange-400)] [--hue-deep:var(--color-orange-600)] [--soft:var(--color-orange-100)] [--ink:var(--color-orange-950)]\",\n};\n\nexport interface CarrierBirdProps {\n  recipient?: string;\n  hue?: CarrierBirdHue;\n  onSend?: () => void;\n  className?: string;\n}\n\ntype Phase = \"idle\" | \"sending\" | \"sent\" | \"returning\";\n\nconst VB = { x: 80, y: 100, w: 240, h: 116 };\nconst GROUND = 206;\nconst REACH = 40;\nconst ENV = { x: 220, y: 168, w: 56, h: 38 };\nconst TRAIL = \"M188 158 C 236 146 300 96 392 4\";\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\n\nfunction useCalmMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nfunction Envelope({ dx = 0 }: { dx?: number }) {\n  const x = ENV.x - dx;\n  return (\n    <g>\n      <rect className=\"fill-card stroke-foreground/30\" height={ENV.h} rx={4} strokeWidth={1.5} width={ENV.w} x={x} y={ENV.y} />\n      <path\n        className=\"fill-none stroke-foreground/30\"\n        d={`M${x + 2} ${ENV.y + 2} L${x + ENV.w / 2} ${ENV.y + 22} L${x + ENV.w - 2} ${ENV.y + 2}`}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth={1.5}\n      />\n      <circle className=\"fill-rose-400\" cx={x + ENV.w / 2} cy={ENV.y + 22} r={5} />\n      <circle className=\"fill-white/60\" cx={x + ENV.w / 2 - 1.6} cy={ENV.y + 20.4} r={1.4} />\n    </g>\n  );\n}\n\nexport function CarrierBird({ recipient = \"Mira\", hue = \"sky\", onSend, className }: CarrierBirdProps) {\n  const uid = useId().replace(/:/g, \"\");\n  const reduce = useCalmMotion();\n  const root = useRef<HTMLDivElement>(null);\n  const inView = useInView(root);\n  const live = inView && !reduce;\n  const [phase, setPhase] = useState<Phase>(\"idle\");\n  const [carrying, setCarrying] = useState(false);\n  const [flapping, setFlapping] = useState(false);\n\n  const birdX = useMotionValue(0);\n  const birdY = useMotionValue(0);\n  const birdRotate = useMotionValue(0);\n  const facing = useMotionValue(1);\n  const squash = useMotionValue(1);\n  const stretch = useTransform(squash, (s) => 1 + (1 - s) * 0.6);\n  const birdOpacity = useMotionValue(1);\n  const carryRotate = useMotionValue(0);\n  const envY = useMotionValue(0);\n  const envOpacity = useMotionValue(1);\n  const trail = useMotionValue(0);\n  const trailOpacity = useMotionValue(0);\n  const tuck = useMotionValue(0);\n  const legScale = useTransform(tuck, (t) => 1 - t);\n  const shadowScale = useTransform(birdY, (y) => Math.max(0, 1 + y / 90));\n  const lid = useMotionValue(1);\n\n  const runId = useRef(0);\n  const running = useRef<AnimationPlaybackControls[]>([]);\n  const timers = useRef(new Set<ReturnType<typeof setTimeout>>());\n\n  useEffect(() => {\n    const pending = timers.current;\n    return () => {\n      runId.current++;\n      for (const t of pending) clearTimeout(t);\n      for (const c of running.current) c.stop();\n    };\n  }, []);\n\n  const wait = (ms: number) =>\n    new Promise<void>((resolve) => {\n      const t = setTimeout(() => {\n        timers.current.delete(t);\n        resolve();\n      }, ms);\n      timers.current.add(t);\n    });\n\n  const run = (c: AnimationPlaybackControls) => {\n    running.current.push(c);\n    return c;\n  };\n\n  const hop = async (toX: number, height: number, ok: () => boolean, track = run) => {\n    track(animate(squash, [1, 0.86, 1], { duration: 0.14, ease: \"easeOut\" }));\n    await wait(90);\n    if (!ok()) return;\n    track(animate(birdX, toX, { duration: 0.34, ease: \"easeInOut\" }));\n    await track(animate(birdY, [0, -height, 0], { duration: 0.34, ease: [\"easeOut\", \"easeIn\"], times: [0, 0.45, 1] }));\n    if (!ok()) return;\n    await track(animate(squash, [1, 0.84, 1], { duration: 0.22, ease: EASE_OUT }));\n  };\n\n  useEffect(() => {\n    if (!live || phase !== \"idle\") return;\n    let alive = true;\n    const own: AnimationPlaybackControls[] = [];\n    const track = (c: AnimationPlaybackControls) => {\n      own.push(c);\n      return c;\n    };\n    const loop = async () => {\n      await wait(700);\n      while (alive) {\n        const roll = Math.random();\n        if (roll < 0.35) {\n          await hop(birdX.get(), 14, () => alive, track);\n        } else if (roll < 0.55) {\n          await hop(birdX.get() > 6 ? 0 : 12, 10, () => alive, track);\n          if (!alive) return;\n          await hop(birdX.get(), 12, () => alive, track);\n        } else if (roll < 0.72) {\n          await track(animate(birdRotate, 16, { duration: 0.16, ease: \"easeOut\" }));\n          await track(animate(birdRotate, [16, 10, 16, 0], { duration: 0.42, ease: \"easeInOut\" }));\n        } else if (roll < 0.86) {\n          await track(animate(facing, -1, { duration: 0.16, ease: \"easeInOut\" }));\n          await wait(700 + Math.random() * 500);\n          if (!alive) return;\n          await track(animate(facing, 1, { duration: 0.16, ease: \"easeInOut\" }));\n        } else {\n          await track(animate(lid, [1, 0.1, 1], { duration: 0.2, times: [0, 0.4, 1] }));\n        }\n        if (!alive) return;\n        await wait(700 + Math.random() * 1500);\n      }\n    };\n    loop();\n    return () => {\n      alive = false;\n      for (const c of own) c.stop();\n    };\n  }, [live, phase]);\n\n  useEffect(() => {\n    if (!live || phase === \"sending\" || phase === \"returning\") return;\n    let timer: ReturnType<typeof setTimeout>;\n    const schedule = () => {\n      timer = setTimeout(\n        () => {\n          animate(lid, [1, 0.1, 1], { duration: 0.2, times: [0, 0.4, 1] });\n          schedule();\n        },\n        2400 + Math.random() * 3200,\n      );\n    };\n    schedule();\n    return () => clearTimeout(timer);\n  }, [live, phase, lid]);\n\n  const send = async () => {\n    if (phase !== \"idle\") return;\n    const id = ++runId.current;\n    const alive = () => runId.current === id;\n    for (const c of running.current) c.stop();\n    running.current = [];\n    setPhase(\"sending\");\n    onSend?.();\n\n    if (reduce) {\n      birdRotate.set(0);\n      facing.set(1);\n      run(animate(envOpacity, 0, { duration: 0.3 }));\n      await run(animate(birdOpacity, 0, { duration: 0.3 }));\n      if (!alive()) return;\n      setPhase(\"sent\");\n      return;\n    }\n\n    run(animate(birdRotate, 0, { duration: 0.12 }));\n    run(animate(facing, 1, { duration: 0.12 }));\n    run(animate(birdY, 0, { duration: 0.12 }));\n    await hop(REACH, 14, alive);\n    if (!alive()) return;\n    await run(animate(birdRotate, 16, { duration: 0.16, ease: \"easeOut\" }));\n    if (!alive()) return;\n    envOpacity.set(0);\n    setCarrying(true);\n    setFlapping(true);\n    run(animate(tuck, 1, { duration: 0.25, ease: \"easeOut\" }));\n    run(animate(carryRotate, 12, { duration: 0.5, ease: EASE_OUT }));\n    run(animate(birdRotate, -8, { duration: 0.35, ease: \"easeOut\" }));\n    await run(animate(birdY, -26, { duration: 0.34, ease: EASE_OUT }));\n    if (!alive()) return;\n    trail.set(0);\n    trailOpacity.set(1);\n    run(animate(trail, 1, { duration: 0.85, ease: \"easeIn\" }));\n    run(animate(birdRotate, -18, { duration: 0.75, ease: \"easeIn\" }));\n    run(animate(birdY, [-26, -64, -130, -240], { duration: 0.85, ease: \"easeIn\" }));\n    await run(animate(birdX, [REACH, 110, 200, 330], { duration: 0.85, ease: \"easeIn\" }));\n    if (!alive()) return;\n    setFlapping(false);\n    setPhase(\"sent\");\n    await run(animate(trailOpacity, 0, { duration: 0.8, ease: \"easeOut\" }));\n  };\n\n  const again = async () => {\n    if (phase !== \"sent\") return;\n    const id = ++runId.current;\n    const alive = () => runId.current === id;\n    for (const c of running.current) c.stop();\n    running.current = [];\n    setPhase(\"returning\");\n    setCarrying(false);\n    trailOpacity.set(0);\n    carryRotate.set(0);\n\n    if (reduce) {\n      birdX.set(0);\n      birdY.set(0);\n      birdRotate.set(0);\n      tuck.set(0);\n      envY.set(0);\n      run(animate(envOpacity, 1, { duration: 0.3 }));\n      await run(animate(birdOpacity, 1, { duration: 0.3 }));\n      if (alive()) setPhase(\"idle\");\n      return;\n    }\n\n    envY.set(-22);\n    run(animate(envOpacity, 1, { duration: 0.2 }));\n    run(animate(envY, 0, { type: \"spring\", stiffness: 520, damping: 18 }));\n    await wait(160);\n    if (!alive()) return;\n    setFlapping(true);\n    birdRotate.set(12);\n    run(animate(birdRotate, [12, 6, 0], { duration: 0.8, ease: \"easeOut\" }));\n    run(animate(birdY, [-150, -40, -6, 0], { duration: 0.8, ease: \"easeOut\", times: [0, 0.55, 0.85, 1] }));\n    await run(animate(birdX, [-230, -70, -8, 0], { duration: 0.8, ease: \"easeOut\", times: [0, 0.55, 0.85, 1] }));\n    if (!alive()) return;\n    setFlapping(false);\n    run(animate(tuck, 0, { duration: 0.15 }));\n    await run(animate(squash, [1, 0.82, 1], { duration: 0.3, ease: EASE_OUT }));\n    if (alive()) setPhase(\"idle\");\n  };\n\n  const sent = phase === \"sent\";\n\n  return (\n    <div\n      ref={root}\n      className={cn(\n        \"relative w-full max-w-[420px] overflow-hidden rounded-2xl border bg-card p-4 shadow-xs/5 sm:p-6\",\n        hues[hue],\n        className,\n      )}\n    >\n      <div\n        aria-hidden\n        className=\"dot-grid pointer-events-none absolute inset-0 opacity-50 [mask-image:radial-gradient(closest-side,black,transparent)]\"\n      />\n      <div className=\"relative mx-auto aspect-[60/29] w-full max-w-[380px]\">\n        <svg aria-hidden className=\"absolute inset-0 size-full overflow-visible\" viewBox={`${VB.x} ${VB.y} ${VB.w} ${VB.h}`}>\n          <defs>\n            <mask id={`${uid}-trail`} maskUnits=\"userSpaceOnUse\">\n              <motion.path d={TRAIL} fill=\"none\" stroke=\"white\" strokeWidth={8} style={{ pathLength: trail }} />\n            </mask>\n          </defs>\n\n          <line className=\"stroke-foreground/10\" strokeLinecap=\"round\" strokeWidth={1.5} x1={92} x2={308} y1={GROUND} y2={GROUND} />\n\n          <motion.g style={{ opacity: trailOpacity }}>\n            <path\n              className=\"fill-none stroke-(--hue)\"\n              d={TRAIL}\n              mask={`url(#${uid}-trail)`}\n              strokeDasharray=\"0.5 8\"\n              strokeLinecap=\"round\"\n              strokeWidth={3}\n            />\n          </motion.g>\n\n          <motion.ellipse\n            className=\"fill-foreground/8\"\n            cx={152}\n            cy={GROUND + 0.5}\n            rx={20}\n            ry={3.5}\n            style={{ opacity: birdOpacity, originX: \"152px\", originY: `${GROUND}px`, scale: shadowScale, transformBox: \"view-box\", x: birdX }}\n          />\n          <motion.g style={{ opacity: envOpacity }}>\n            <ellipse className=\"fill-foreground/8\" cx={ENV.x + ENV.w / 2} cy={GROUND + 0.5} rx={30} ry={3} />\n            <motion.g style={{ y: envY }}>\n              <Envelope />\n            </motion.g>\n          </motion.g>\n\n          <motion.g\n            animate={sent ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.9 }}\n            initial={false}\n            style={{ originX: \"200px\", originY: \"150px\", transformBox: \"view-box\" }}\n            transition={{ duration: reduce ? 0.2 : 0.35, ease: EASE_OUT, delay: sent && !reduce ? 0.1 : 0 }}\n          >\n            <circle className=\"fill-success/15\" cx={200} cy={150} r={38} />\n            <circle className=\"fill-success\" cx={200} cy={150} r={26} />\n            <motion.path\n              animate={{ pathLength: sent ? 1 : 0 }}\n              className=\"fill-none stroke-white\"\n              d=\"M189 151 l7.5 7.5 l14 -15\"\n              initial={false}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeWidth={4.5}\n              transition={{ duration: reduce ? 0 : 0.35, ease: \"easeOut\", delay: sent && !reduce ? 0.3 : 0 }}\n            />\n          </motion.g>\n\n          <motion.g style={{ opacity: birdOpacity, rotate: birdRotate, x: birdX, y: birdY, originX: \"152px\", originY: \"200px\", transformBox: \"view-box\" }}>\n            <motion.g style={{ originX: \"152px\", originY: `${GROUND}px`, scaleX: stretch, scaleY: squash, transformBox: \"view-box\" }}>\n              <motion.g style={{ originX: \"152px\", originY: \"180px\", scaleX: facing, transformBox: \"view-box\" }}>\n                <motion.g\n                  className=\"stroke-amber-500\"\n                  strokeLinecap=\"round\"\n                  strokeWidth={2.5}\n                  style={{ originX: \"152px\", originY: \"196px\", scaleY: legScale, transformBox: \"view-box\" }}\n                >\n                  <path className=\"fill-none\" d=\"M146 196 L144 206 M140.5 206 H148\" />\n                  <path className=\"fill-none\" d=\"M158 196 L160 206 M156 206 H163.5\" />\n                </motion.g>\n                <path\n                  className=\"fill-(--hue-deep) stroke-(--hue-deep)\"\n                  d=\"M134 167 L113 157 Q108 159 111 164 L120 170 L110 173 Q108 178 113 179 L134 181 Z\"\n                  strokeLinejoin=\"round\"\n                  strokeWidth={2.5}\n                />\n                <path className=\"fill-none stroke-(--hue-deep)\" d=\"M151 157 q-2 -8 6 -10\" strokeLinecap=\"round\" strokeWidth={4} />\n                <circle className=\"fill-(--hue)\" cx={152} cy={178} r={23} />\n                <ellipse className=\"fill-(--soft)\" cx={159} cy={186} rx={13} ry={11.5} />\n                <path className=\"fill-none stroke-white/40\" d=\"M137 168 q5 -9 15 -10\" strokeLinecap=\"round\" strokeWidth={3.5} />\n                <ellipse className=\"fill-rose-400/50\" cx={166} cy={180} rx={4.2} ry={2.6} />\n                <motion.g style={{ originX: \"162px\", originY: \"171px\", scaleY: lid, transformBox: \"view-box\" }}>\n                  <ellipse className=\"fill-(--ink)\" cx={162} cy={171} rx={3.9} ry={4.9} />\n                  <circle className=\"fill-white\" cx={160.7} cy={169.1} r={1.5} />\n                </motion.g>\n                <path className=\"fill-amber-400 stroke-amber-400\" d=\"M173 173.5 L183 177 L173 180.5 Z\" strokeLinejoin=\"round\" strokeWidth={2} />\n\n                {carrying && (\n                  <motion.g style={{ originX: `${ENV.x - REACH + 2}px`, originY: `${ENV.y + 2}px`, rotate: carryRotate, transformBox: \"view-box\" }}>\n                    <Envelope dx={REACH} />\n                  </motion.g>\n                )}\n\n                <motion.g\n                  animate={flapping && !reduce ? { rotate: [0, 70, -10, 0] } : { rotate: 0 }}\n                  style={{ originX: \"148px\", originY: \"176px\", transformBox: \"view-box\" }}\n                  transition={flapping && !reduce ? { duration: 0.22, ease: \"easeInOut\", repeat: Number.POSITIVE_INFINITY } : { duration: 0.2 }}\n                >\n                  <path\n                    className=\"fill-(--hue-deep)\"\n                    d=\"M148 173 C 137 172 128 182 131 193 C 137 198 151 191 153 181 C 154 176 152 173 148 173 Z\"\n                  />\n                </motion.g>\n              </motion.g>\n            </motion.g>\n          </motion.g>\n        </svg>\n      </div>\n\n      <div className=\"relative mt-3 flex flex-col items-center gap-3 text-center\">\n        <div aria-live=\"polite\">\n          <p className=\"text-balance font-medium text-foreground text-sm\">\n            {sent ? `Sent to ${recipient}` : phase === \"sending\" ? \"Sending…\" : `Note to ${recipient}`}\n          </p>\n          <p className=\"mx-auto mt-1 min-h-8 max-w-[34ch] text-pretty text-muted-foreground text-xs\">\n            {sent ? \"It's on its way. We'll let you know when they reply.\" : \"Sealed and ready. The bird will take it from here.\"}\n          </p>\n        </div>\n        {sent ? (\n          <Button className=\"min-w-32 active:scale-[0.97]\" onClick={again} size=\"sm\" variant=\"outline\">\n            <RotateCcwIcon />\n            Send another\n          </Button>\n        ) : (\n          <Button className=\"min-w-32 active:scale-[0.97]\" disabled={phase !== \"idle\"} onClick={send} size=\"sm\">\n            <SendIcon />\n            Send\n          </Button>\n        )}\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}