{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "desk-bot",
  "type": "registry:component",
  "title": "Desk Bot",
  "description": "A little desk robot that follows your cursor with glowing eyes, perks its antenna when you come close, and nods off after a few quiet seconds until you tap it awake.",
  "categories": [
    "illustration",
    "Characters"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/desk-bot.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport {\n  type AnimationPlaybackControls,\n  animate,\n  motion,\n  type MotionValue,\n  useInView,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n} from \"motion/react\";\nimport { type PointerEvent, useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type DeskBotHue = \"sky\" | \"emerald\" | \"amber\" | \"violet\" | \"rose\" | \"orange\" | \"indigo\" | \"teal\";\n\nconst hues: Record<DeskBotHue, string> = {\n  sky: \"[--hue-hi:var(--color-sky-300)] [--hue:var(--color-sky-400)] [--hue-deep:var(--color-sky-600)] [--ink:var(--color-sky-950)] [--glow:var(--color-sky-200)]\",\n  emerald:\n    \"[--hue-hi:var(--color-emerald-300)] [--hue:var(--color-emerald-400)] [--hue-deep:var(--color-emerald-600)] [--ink:var(--color-emerald-950)] [--glow:var(--color-emerald-200)]\",\n  amber:\n    \"[--hue-hi:var(--color-amber-300)] [--hue:var(--color-amber-400)] [--hue-deep:var(--color-amber-600)] [--ink:var(--color-amber-950)] [--glow:var(--color-amber-200)]\",\n  violet:\n    \"[--hue-hi:var(--color-violet-300)] [--hue:var(--color-violet-400)] [--hue-deep:var(--color-violet-600)] [--ink:var(--color-violet-950)] [--glow:var(--color-violet-200)]\",\n  rose: \"[--hue-hi:var(--color-rose-300)] [--hue:var(--color-rose-400)] [--hue-deep:var(--color-rose-600)] [--ink:var(--color-rose-950)] [--glow:var(--color-rose-200)]\",\n  orange:\n    \"[--hue-hi:var(--color-orange-300)] [--hue:var(--color-orange-400)] [--hue-deep:var(--color-orange-600)] [--ink:var(--color-orange-950)] [--glow:var(--color-orange-200)]\",\n  indigo:\n    \"[--hue-hi:var(--color-indigo-300)] [--hue:var(--color-indigo-400)] [--hue-deep:var(--color-indigo-600)] [--ink:var(--color-indigo-950)] [--glow:var(--color-indigo-200)]\",\n  teal: \"[--hue-hi:var(--color-teal-300)] [--hue:var(--color-teal-400)] [--hue-deep:var(--color-teal-600)] [--ink:var(--color-teal-950)] [--glow:var(--color-teal-200)]\",\n};\n\nexport interface DeskBotProps {\n  name?: string;\n  hue?: DeskBotHue;\n  idleAfter?: number;\n  onWake?: () => void;\n  onDoze?: () => void;\n  className?: string;\n}\n\nconst VB = { x: 20, y: 28, w: 360, h: 212 };\nconst FACE = { x: 200, y: 140 };\n\nconst zs = [\n  { size: 13, x: 270, y: 82, delay: 0 },\n  { size: 17, x: 286, y: 64, delay: 0.9 },\n  { size: 22, x: 304, y: 44, delay: 1.8 },\n];\n\nconst steam = [\n  { x: 76, delay: 0 },\n  { x: 86, delay: 1.3 },\n];\n\nfunction useCalmMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nfunction useBlink(active: boolean) {\n  const open = useMotionValue(1);\n  useEffect(() => {\n    if (!active) {\n      open.set(1);\n      return;\n    }\n    let timer: ReturnType<typeof setTimeout>;\n    let blink: AnimationPlaybackControls | undefined;\n    const schedule = () => {\n      timer = setTimeout(\n        () => {\n          const twice = Math.random() < 0.22;\n          blink = animate(open, twice ? [1, 0.08, 1, 0.08, 1] : [1, 0.08, 1], {\n            duration: twice ? 0.4 : 0.19,\n            ease: \"easeInOut\",\n            times: twice ? [0, 0.18, 0.5, 0.68, 1] : [0, 0.38, 1],\n          });\n          schedule();\n        },\n        2000 + Math.random() * 4200,\n      );\n    };\n    schedule();\n    return () => {\n      clearTimeout(timer);\n      blink?.stop();\n    };\n  }, [active, open]);\n  return open;\n}\n\nfunction Eye({\n  cx,\n  near,\n  scaleY,\n}: {\n  cx: number;\n  near: boolean;\n  scaleY: MotionValue<number>;\n}) {\n  return (\n    <motion.g\n      animate={{ scale: near ? 1.12 : 1 }}\n      initial={false}\n      style={{ originX: `${cx}px`, originY: `${FACE.y}px`, transformBox: \"view-box\" }}\n      transition={{ type: \"spring\", stiffness: 420, damping: 22 }}\n    >\n      <motion.rect\n        className=\"fill-(--glow)\"\n        height={26}\n        rx={8}\n        style={{ originX: `${cx}px`, originY: `${FACE.y}px`, scaleY, transformBox: \"view-box\" }}\n        width={16}\n        x={cx - 8}\n        y={FACE.y - 13}\n      />\n    </motion.g>\n  );\n}\n\nexport function DeskBot({ name = \"Pip\", hue = \"sky\", idleAfter = 6000, onWake, onDoze, className }: DeskBotProps) {\n  const uid = useId().replace(/:/g, \"\");\n  const reduce = useCalmMotion();\n  const root = useRef<HTMLDivElement>(null);\n  const svg = useRef<SVGSVGElement>(null);\n  const inView = useInView(root);\n  const live = inView && !reduce;\n\n  const [dozing, setDozing] = useState(false);\n  const [near, setNear] = useState(false);\n  const [tracking, setTracking] = useState(false);\n  const [cheer, setCheer] = useState(0);\n\n  const gazeX = useMotionValue(0);\n  const gazeY = useMotionValue(0);\n  const lookX = useSpring(gazeX, { stiffness: 260, damping: 26, mass: 0.6 });\n  const lookY = useSpring(gazeY, { stiffness: 260, damping: 26, mass: 0.6 });\n  const eyesX = useTransform(lookX, (v) => v * 13);\n  const eyesY = useTransform(lookY, (v) => v * 8);\n  const headTilt = useTransform(lookX, (v) => v * 4);\n  const headShift = useTransform(lookX, (v) => v * 3);\n\n  const lid = useBlink(live && !dozing);\n  const pop = useMotionValue(1);\n  const jolt = useMotionValue(0);\n  const eyeScale = useTransform([lid, pop] as const, ([l, p]: number[]) => l * p);\n\n  const idle = useRef<ReturnType<typeof setTimeout>>(undefined);\n  const cheerTimer = useRef<ReturnType<typeof setTimeout>>(undefined);\n  const callbacks = useRef({ onWake, onDoze });\n\n  useEffect(() => {\n    callbacks.current = { onWake, onDoze };\n  });\n\n  const stir = useCallback(() => {\n    clearTimeout(idle.current);\n    idle.current = setTimeout(() => {\n      setDozing(true);\n      setNear(false);\n      callbacks.current.onDoze?.();\n    }, idleAfter);\n  }, [idleAfter]);\n\n  useEffect(() => {\n    if (dozing || !inView) {\n      clearTimeout(idle.current);\n      return;\n    }\n    stir();\n    return () => clearTimeout(idle.current);\n  }, [dozing, inView, stir]);\n\n  useEffect(() => () => clearTimeout(cheerTimer.current), []);\n\n  useEffect(() => {\n    if (dozing) {\n      gazeX.set(0);\n      gazeY.set(0.55);\n      return;\n    }\n    if (!live || tracking) return;\n    let timer: ReturnType<typeof setTimeout>;\n    const look = () => {\n      const centre = Math.random() < 0.35;\n      gazeX.set(centre ? 0 : (Math.random() * 2 - 1) * 0.85);\n      gazeY.set(centre ? 0 : (Math.random() * 2 - 1) * 0.55);\n      timer = setTimeout(look, 1400 + Math.random() * 2400);\n    };\n    timer = setTimeout(look, 700);\n    return () => clearTimeout(timer);\n  }, [live, tracking, dozing, gazeX, gazeY]);\n\n  useEffect(() => {\n    if (!reduce) return;\n    gazeX.set(0);\n    gazeY.set(dozing ? 0.55 : 0);\n  }, [reduce, dozing, gazeX, gazeY]);\n\n  const follow = (e: PointerEvent<HTMLDivElement>) => {\n    if (dozing) return;\n    stir();\n    if (e.pointerType === \"touch\" || reduce) return;\n    const box = svg.current?.getBoundingClientRect();\n    if (!box?.width) return;\n    const unit = box.width / VB.w;\n    const dx = (e.clientX - box.left) / unit + VB.x - FACE.x;\n    const dy = (e.clientY - box.top) / unit + VB.y - FACE.y;\n    const reach = Math.hypot(dx, dy, 80);\n    gazeX.set(dx / reach);\n    gazeY.set(dy / reach);\n    setTracking(true);\n    setNear(Math.hypot(dx, dy) < 150);\n  };\n\n  const release = () => {\n    setTracking(false);\n    setNear(false);\n    gazeX.set(0);\n    gazeY.set(0);\n  };\n\n  const poke = () => {\n    stir();\n    if (dozing) {\n      setDozing(false);\n      callbacks.current.onWake?.();\n      if (!reduce) {\n        animate(pop, [0.2, 1.18, 1], { duration: 0.45, ease: [0.23, 1, 0.32, 1], times: [0, 0.55, 1] });\n        animate(jolt, [0, -10, 0], { duration: 0.45, ease: [0.23, 1, 0.32, 1], times: [0, 0.4, 1] });\n      }\n      return;\n    }\n    setCheer((c) => c + 1);\n    clearTimeout(cheerTimer.current);\n    cheerTimer.current = setTimeout(() => setCheer(0), 1100);\n    if (!reduce) animate(jolt, [0, -6, 0], { duration: 0.36, ease: [0.23, 1, 0.32, 1], times: [0, 0.4, 1] });\n  };\n\n  const happy = cheer > 0 && !dozing;\n  const eyes = dozing ? \"closed\" : happy ? \"happy\" : \"open\";\n  const perked = (near || happy) && !dozing;\n  const settle = { duration: reduce ? 0 : 1.1, ease: [0.65, 0, 0.35, 1] as const };\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      onPointerLeave={release}\n      onPointerMove={follow}\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      <button\n        aria-label={dozing ? `${name} is asleep. Wake ${name} up` : `Give ${name} a pat`}\n        className=\"relative mx-auto block aspect-[90/53] w-full max-w-[380px] cursor-pointer rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n        onBlur={() => setNear(false)}\n        onClick={poke}\n        onFocus={(e) => e.currentTarget.matches(\":focus-visible\") && !dozing && setNear(true)}\n        type=\"button\"\n      >\n        <svg aria-hidden className=\"absolute inset-0 size-full overflow-visible\" ref={svg} viewBox={`${VB.x} ${VB.y} ${VB.w} ${VB.h}`}>\n          <defs>\n            <linearGradient id={`${uid}-shell`} x1=\"0.15\" x2=\"0.85\" y1=\"0\" y2=\"1\">\n              <stop offset=\"0\" stopColor=\"var(--hue-hi)\" />\n              <stop offset=\"1\" stopColor=\"var(--hue)\" />\n            </linearGradient>\n          </defs>\n\n          <line className=\"stroke-foreground/10\" strokeLinecap=\"round\" strokeWidth={1.5} x1={36} x2={364} y1={229} y2={229} />\n          <motion.ellipse\n            animate={live && !dozing ? { rx: [64, 60, 64] } : { rx: 64 }}\n            className=\"fill-foreground/8\"\n            cx={200}\n            cy={229}\n            ry={5}\n            transition={live && !dozing ? { duration: 3.2, ease: \"easeInOut\", repeat: Number.POSITIVE_INFINITY } : { duration: 0.6 }}\n          />\n\n          <g>\n            <rect className=\"fill-card stroke-foreground/25\" height={30} rx={6} strokeWidth={1.5} width={30} x={62} y={198} />\n            <path className=\"fill-none stroke-foreground/25\" d=\"M92 206 h4 a7 7 0 0 1 0 14 h-4\" strokeWidth={1.5} />\n            <rect className=\"fill-(--hue)\" height={5} width={28.5} x={62.75} y={208} />\n            {live &&\n              steam.map((s) => (\n                <motion.path\n                  animate={{ opacity: [0, 0.7, 0], y: [4, -6, -14] }}\n                  className=\"fill-none stroke-foreground/20\"\n                  d={`M${s.x} 190 q -4 -5 0 -10 q 4 -5 0 -10`}\n                  initial={{ opacity: 0 }}\n                  key={s.x}\n                  strokeLinecap=\"round\"\n                  strokeWidth={2}\n                  transition={{ delay: s.delay, duration: 2.6, ease: \"easeOut\", repeat: Number.POSITIVE_INFINITY }}\n                />\n              ))}\n          </g>\n\n          <g>\n            <rect className=\"fill-card stroke-foreground/25\" height={12} rx={3} strokeWidth={1.5} width={58} x={296} y={216} />\n            <rect className=\"fill-(--hue-hi)\" height={12} rx={3} width={52} x={302} y={204} />\n            <line className=\"stroke-black/10\" strokeWidth={1.5} x1={310} x2={346} y1={210} y2={210} />\n            <line className=\"stroke-foreground/15\" strokeWidth={1.5} x1={302} x2={348} y1={222} y2={222} />\n          </g>\n\n          <motion.g\n            animate={live ? (dozing ? { y: [3, 5, 3] } : { y: [0, -2, 0] }) : { y: dozing ? 3 : 0 }}\n            transition={\n              live ? { duration: dozing ? 3.8 : 2.8, ease: \"easeInOut\", repeat: Number.POSITIVE_INFINITY } : { duration: 0.6 }\n            }\n          >\n            <rect className=\"fill-(--hue-deep)\" height={24} rx={10} width={72} x={164} y={205} />\n            <rect className=\"fill-white/20\" height={3} rx={1.5} width={52} x={174} y={209} />\n            <rect className=\"fill-(--hue-deep)\" height={18} width={20} x={190} y={192} />\n\n            <motion.g style={{ y: jolt }}>\n              <motion.g\n                style={{ rotate: headTilt, x: headShift, originX: \"200px\", originY: \"200px\", transformBox: \"view-box\" }}\n              >\n                <motion.g\n                  animate={\n                    happy && !reduce\n                      ? { rotate: [0, -16, 12, -6, 0], y: -9 }\n                      : perked\n                        ? { rotate: 0, y: -9 }\n                        : dozing\n                          ? { rotate: 26, y: 3 }\n                          : { rotate: 0, y: 0 }\n                  }\n                  initial={false}\n                  style={{ originX: \"200px\", originY: \"96px\", transformBox: \"view-box\" }}\n                  transition={\n                    reduce\n                      ? { duration: 0 }\n                      : happy\n                        ? { duration: 0.6, ease: \"easeOut\" }\n                        : dozing\n                          ? settle\n                          : { type: \"spring\", stiffness: 480, damping: 13 }\n                  }\n                >\n                  <line className=\"stroke-(--hue-deep)\" strokeLinecap=\"round\" strokeWidth={4} x1={200} x2={200} y1={100} y2={60} />\n                  <circle className=\"fill-(--hue-deep)\" cx={200} cy={56} r={8} />\n                  <motion.circle\n                    animate={{ opacity: perked ? 1 : 0 }}\n                    className=\"fill-amber-300\"\n                    cx={200}\n                    cy={56}\n                    initial={false}\n                    r={8}\n                    transition={{ duration: 0.2 }}\n                  />\n                  <motion.circle\n                    animate={perked && live ? { opacity: [0.5, 0], scale: [1, 1.9] } : { opacity: 0, scale: 1 }}\n                    className=\"fill-amber-300/40\"\n                    cx={200}\n                    cy={56}\n                    r={8}\n                    style={{ originX: \"200px\", originY: \"56px\", transformBox: \"view-box\" }}\n                    transition={perked && live ? { duration: 1.2, ease: \"easeOut\", repeat: Number.POSITIVE_INFINITY } : { duration: 0.2 }}\n                  />\n                  <circle className=\"fill-white/50\" cx={197} cy={53} r={2.2} />\n                </motion.g>\n\n                <rect className=\"fill-(--hue-deep)\" height={40} rx={6} width={16} x={114} y={124} />\n                <rect className=\"fill-(--hue-deep)\" height={40} rx={6} width={16} x={270} y={124} />\n                <rect fill={`url(#${uid}-shell)`} height={118} rx={36} width={152} x={124} y={84} />\n                <path className=\"fill-none stroke-white/45\" d=\"M146 110 q6 -14 24 -18\" strokeLinecap=\"round\" strokeWidth={5} />\n                <rect className=\"fill-(--ink)\" height={78} rx={24} width={116} x={142} y={102} />\n                <path className=\"fill-white/[0.06]\" d=\"M166 102 h40 l-30 78 h-10 a24 24 0 0 1 -24 -24 v-30 a24 24 0 0 1 24 -24 Z\" />\n\n                <motion.g\n                  animate={{ opacity: perked ? 0.85 : dozing ? 0.35 : 0.55 }}\n                  className=\"fill-rose-400\"\n                  initial={false}\n                  transition={{ duration: 0.3 }}\n                >\n                  <ellipse cx={162} cy={160} rx={8} ry={4.5} />\n                  <ellipse cx={238} cy={160} rx={8} ry={4.5} />\n                </motion.g>\n\n                <motion.g className=\"[filter:drop-shadow(0_0_5px_var(--glow))]\" style={{ x: eyesX, y: eyesY }}>\n                  <motion.g\n                    animate={{ opacity: eyes === \"open\" ? 1 : 0 }}\n                    initial={false}\n                    transition={{ duration: eyes === \"closed\" ? 0.5 : 0.12 }}\n                  >\n                    <Eye cx={178} near={perked} scaleY={eyeScale} />\n                    <Eye cx={222} near={perked} scaleY={eyeScale} />\n                  </motion.g>\n                  <motion.g\n                    animate={{ opacity: eyes === \"happy\" ? 1 : 0 }}\n                    className=\"fill-none stroke-(--glow)\"\n                    initial={false}\n                    strokeLinecap=\"round\"\n                    strokeWidth={4.5}\n                    transition={{ duration: 0.12 }}\n                  >\n                    <path d=\"M169 145 q9 -12 18 0\" />\n                    <path d=\"M213 145 q9 -12 18 0\" />\n                  </motion.g>\n                </motion.g>\n\n                <motion.g\n                  animate={{ opacity: eyes === \"closed\" ? 1 : 0 }}\n                  className=\"fill-none stroke-(--glow)\"\n                  initial={false}\n                  strokeLinecap=\"round\"\n                  strokeWidth={4}\n                  transition={{ duration: eyes === \"closed\" ? 0.6 : 0.12, delay: eyes === \"closed\" && !reduce ? 0.25 : 0 }}\n                >\n                  <path d=\"M170 146 q8 6 16 0\" />\n                  <path d=\"M214 146 q8 6 16 0\" />\n                </motion.g>\n\n                <motion.path\n                  animate={{\n                    d: dozing ? \"M195 165 q5 0 10 0\" : perked ? \"M191 162 q9 10 18 0\" : \"M194 163 q6 6 12 0\",\n                  }}\n                  className=\"fill-none stroke-(--glow)\"\n                  initial={false}\n                  strokeLinecap=\"round\"\n                  strokeWidth={3}\n                  transition={{ duration: reduce ? 0 : 0.25, ease: \"easeOut\" }}\n                />\n              </motion.g>\n            </motion.g>\n          </motion.g>\n\n          {dozing &&\n            zs.map((z) => (\n              <motion.text\n                animate={\n                  live\n                    ? { opacity: [0, 1, 1, 0], x: [0, 6, 10, 14], y: [6, -4, -12, -20] }\n                    : { opacity: 1, x: 0, y: 0 }\n                }\n                className=\"fill-(--hue-deep) font-semibold dark:fill-(--hue-hi)\"\n                fontSize={z.size}\n                initial={{ opacity: 0 }}\n                key={z.x}\n                transition={\n                  live\n                    ? { delay: 0.6 + z.delay, duration: 2.7, ease: \"easeOut\", repeat: Number.POSITIVE_INFINITY, times: [0, 0.25, 0.7, 1] }\n                    : { duration: 0.3 }\n                }\n                x={z.x}\n                y={z.y}\n              >\n                z\n              </motion.text>\n            ))}\n        </svg>\n      </button>\n\n      <div className=\"relative mt-3 text-center\" aria-live=\"polite\">\n        <p className=\"text-balance font-medium text-foreground text-sm\">\n          {dozing ? `${name} dozed off` : `${name} is keeping you company`}\n        </p>\n        <p className=\"mx-auto mt-1 min-h-8 max-w-[34ch] text-pretty text-muted-foreground text-xs\">\n          {dozing ? `You stepped away for a bit. Give ${name} a tap to wake it.` : `Nothing needs you right now. Step away and ${name} will nap.`}\n        </p>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}