{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"boop","type":"registry:ui","title":"Boop","description":"Gives an icon a small springy nudge when the control around it is hovered or focused, then lets it settle back on its own even if the pointer stays: a bell rings, a cog turns, an arrow leans forward.","dependencies":["clsx","tailwind-merge"],"registryDependencies":["https://caliperui.site/r/styles.json"],"files":[{"path":"ui/boop.tsx","type":"registry:ui","target":"components/ui/boop.tsx","content":"\"use client\";\nimport { useEffect, useRef, useState, type ComponentProps, type CSSProperties } from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./boop.css\";\n\nexport type BoopProps = ComponentProps<\"span\"> & {\n  /** Degrees to turn. */\n  rotate?: number;\n  /** Size at the peak, 1 for none. */\n  scale?: number;\n  /** Pixels to shift sideways and down. */\n  x?: number;\n  y?: number;\n  /** How long the push lasts before the spring brings it back, in ms. */\n  timing?: number;\n};\n\n/**\n * Gives an icon a small springy nudge when the control around it is hovered or focused, then lets it settle back on\n * its own even if the pointer stays. Wrap the icon inside the button or link; the parent is what triggers it. Under\n * reduced motion nothing moves.\n */\nexport function Boop({ rotate = 0, scale = 1, x = 0, y = 0, timing = 160, className, style, children, ...props }: BoopProps) {\n  const root = useRef<HTMLSpanElement>(null);\n  const [on, setOn] = useState(false);\n\n  useEffect(() => {\n    const host = root.current?.parentElement;\n    if (!host) return;\n    const trigger = () => setOn(true);\n    host.addEventListener(\"pointerenter\", trigger);\n    host.addEventListener(\"focusin\", trigger);\n    return () => {\n      host.removeEventListener(\"pointerenter\", trigger);\n      host.removeEventListener(\"focusin\", trigger);\n    };\n  }, []);\n\n  useEffect(() => {\n    if (!on) return;\n    const timer = window.setTimeout(() => setOn(false), timing);\n    return () => window.clearTimeout(timer);\n  }, [on, timing]);\n\n  return (\n    <span\n      ref={root}\n      data-slot=\"boop\"\n      data-on={on ? \"\" : undefined}\n      className={cn(\"inline-flex\", className)}\n      style={{ \"--boop-r\": `${rotate}deg`, \"--boop-s\": scale, \"--boop-x\": `${x}px`, \"--boop-y\": `${y}px`, ...style } as CSSProperties}\n      {...props}\n    >\n      {children}\n    </span>\n  );\n}\n"},{"path":"ui/lib/cn.ts","type":"registry:ui","target":"components/ui/lib/cn.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\nconst twMerge = extendTailwindMerge({\n  extend: { theme: { radius: [\"control\", \"surface\", \"overlay\"], shadow: [\"overlay\"] } },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"},{"path":"ui/boop.css","type":"registry:file","target":"components/ui/boop.css","content":"/* boop.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* Boop: a quick push, then the settle curve springs it home. */\n  [data-slot=\"boop\"] { transition: transform 0.6s var(--ease-settle); }\n  [data-slot=\"boop\"][data-on] { transform: translate(var(--boop-x), var(--boop-y)) rotate(var(--boop-r)) scale(var(--boop-s)); transition-duration: 0.14s; transition-timing-function: var(--ease-out); }\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"boop\"][data-slot] { transform: none; transition: none; }\n  }\n}\n"}],"categories":["motion"],"meta":{"tier":"free","license":"MIT","family":"surfaces","docs":"/docs/components/boop"}}