{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"smooth-cursor","type":"registry:ui","title":"SmoothCursor","description":"Multiplayer-style cursor: an upright coloured arrow with a white rim follows the pointer on a spring, and a name pill trails it on a softer spring and dims when idle.","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["effects"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/internals-motion.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/smooth-cursor/SmoothCursor.tsx","content":"\"use client\";\n\nimport {\n\tuseCallback,\n\tuseEffect,\n\tuseId,\n\tuseRef,\n\tuseState,\n\ttype CSSProperties,\n\ttype ReactNode,\n} from \"react\";\nimport { useLiveRef } from \"../../internals/dom/use-live-ref\";\nimport { useReducedMotion } from \"../../internals/motion/media-query\";\nimport { cn } from \"../../utils\";\nimport {\n\tcreateSmoothCursor,\n\ttype SmoothCursorEngine,\n\ttype SpringConfig,\n} from \"./smooth-cursor-core\";\nimport \"./smooth-cursor.css\";\n\nexport type { SpringConfig };\n\nexport interface SmoothCursorProps {\n\t/** Custom cursor node to replace the default arrow cursor */\n\tcursor?: ReactNode;\n\t/** Spring physics configuration */\n\tspringConfig?: SpringConfig;\n\t/** Additional CSS classes */\n\tclassName?: string;\n\t/** Name shown in a pill that trails the arrow. No pill when omitted. */\n\tlabel?: string;\n\t/** Fill colour of the arrow and the name pill (any CSS colour) */\n\tcolor?: string;\n\t/** Rotate the cursor toward its direction of travel (the arrow stays upright when false) */\n\trotate?: boolean;\n\t/** Milliseconds without movement before the name pill dims; 0 disables */\n\tidleFade?: number;\n\t/** Spring for the name pill; missing fields derive from `springConfig` (stiffness × 0.55, damping × 1.1) */\n\tlabelSpring?: SpringConfig;\n}\n\n/** Arrow tip inside the 24×24 default arrow — the pointer's hotspot. */\nconst TIP_X = 4;\nconst TIP_Y = 3;\n\nexport function SmoothCursor({\n\tcursor,\n\tspringConfig = {},\n\tclassName = \"\",\n\tlabel,\n\tcolor = \"#0e9f6e\",\n\trotate = false,\n\tidleFade = 1500,\n\tlabelSpring,\n}: SmoothCursorProps) {\n\t// SSR-stable, and stripped to ASCII so the `url(#…)` paint reference never\n\t// has to carry the delimiters React puts in its ids.\n\tconst uid = useId().replace(/[^A-Za-z0-9_-]/g, \"\");\n\tconst fillId = `smooth-cursor-fill-${uid}`;\n\n\tconst cursorElRef = useRef<HTMLDivElement | null>(null);\n\tconst labelElRef = useRef<HTMLDivElement | null>(null);\n\tconst engineRef = useRef<SmoothCursorEngine | null>(null);\n\tconst [visible, setVisible] = useState(false);\n\tconst [idle, setIdle] = useState(false);\n\n\tconst reducedMotion = useReducedMotion();\n\t// The latest props, read once when the engine is created (the Svelte\n\t// source's `untrack` reads at mount).\n\tconst initRef = useLiveRef({ springConfig, labelSpring, rotate, idleFade, reducedMotion });\n\t/** The reduced-motion value the engine currently holds. */\n\tconst appliedReducedMotionRef = useRef(false);\n\n\t// The default arrow is pinned by its tip while upright; a custom cursor, or\n\t// any cursor that rotates with travel, is pinned by its centre (the pivot).\n\tconst hotspot = !cursor && !rotate ? `-${TIP_X}px -${TIP_Y}px` : \"-50% -50%\";\n\n\tuseEffect(() => {\n\t\tconst init = initRef.current;\n\t\t// The mount-time reduced-motion value is a creation option (plain\n\t\t// assignment, no snap); only a later change goes through `setOptions`,\n\t\t// which stops the loop and snaps.\n\t\tappliedReducedMotionRef.current = init.reducedMotion;\n\n\t\tconst engine = createSmoothCursor(\n\t\t\t{ cursor: cursorElRef.current!, label: labelElRef.current },\n\t\t\t{\n\t\t\t\tspringConfig: init.springConfig,\n\t\t\t\tlabelSpring: init.labelSpring,\n\t\t\t\trotate: init.rotate,\n\t\t\t\tidleFade: init.idleFade,\n\t\t\t\treducedMotion: init.reducedMotion,\n\t\t\t\tonVisibleChange: setVisible,\n\t\t\t\tonIdleChange: setIdle,\n\t\t\t}\n\t\t);\n\t\tengineRef.current = engine;\n\n\t\treturn () => {\n\t\t\tengine.destroy();\n\t\t\tengineRef.current = null;\n\t\t};\n\t}, [initRef]);\n\n\tuseEffect(() => {\n\t\tif (appliedReducedMotionRef.current === reducedMotion) return;\n\t\tappliedReducedMotionRef.current = reducedMotion;\n\t\tengineRef.current?.setOptions({ reducedMotion });\n\t}, [reducedMotion]);\n\n\tuseEffect(() => {\n\t\tengineRef.current?.setOptions({ springConfig });\n\t}, [springConfig]);\n\n\tuseEffect(() => {\n\t\tengineRef.current?.setOptions({ labelSpring });\n\t}, [labelSpring]);\n\n\tuseEffect(() => {\n\t\tengineRef.current?.setOptions({ rotate });\n\t}, [rotate]);\n\n\tuseEffect(() => {\n\t\tengineRef.current?.setOptions({ idleFade });\n\t}, [idleFade]);\n\n\t// The pill mounts and unmounts with `label`; hand the live node to the engine.\n\tconst setLabelEl = useCallback((node: HTMLDivElement | null) => {\n\t\tlabelElRef.current = node;\n\t\tengineRef.current?.setLabel(node);\n\t}, []);\n\n\treturn (\n\t\t<div\n\t\t\taria-hidden=\"true\"\n\t\t\tclassName={cn(\n\t\t\t\t\"smooth-cursor pointer-events-none fixed top-0 left-0 z-[9999]\",\n\t\t\t\tvisible ? \"opacity-100\" : \"opacity-0\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\tstyle={{ \"--_sc-color\": `var(--smooth-cursor-color, ${color})` } as CSSProperties}\n\t\t>\n\t\t\t<div\n\t\t\t\tref={cursorElRef}\n\t\t\t\tclassName={rotate ? \"sc-pointer sc-pointer--rotate\" : \"sc-pointer\"}\n\t\t\t\tstyle={{ translate: hotspot }}\n\t\t\t>\n\t\t\t\t{cursor ? (\n\t\t\t\t\tcursor\n\t\t\t\t) : (\n\t\t\t\t\t// Default cursor: coloured arrowhead with a white rim\n\t\t\t\t\t<svg\n\t\t\t\t\t\tclassName=\"sc-arrow\"\n\t\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t\t\twidth=\"24\"\n\t\t\t\t\t\theight=\"24\"\n\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\tfocusable=\"false\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<defs>\n\t\t\t\t\t\t\t<linearGradient id={fillId} x1=\"0.1\" y1=\"0\" x2=\"0.75\" y2=\"1\">\n\t\t\t\t\t\t\t\t<stop offset=\"0\" className=\"sc-stop-hot\" />\n\t\t\t\t\t\t\t\t<stop offset=\"0.55\" className=\"sc-stop-body\" />\n\t\t\t\t\t\t\t\t<stop offset=\"1\" className=\"sc-stop-deep\" />\n\t\t\t\t\t\t\t</linearGradient>\n\t\t\t\t\t\t</defs>\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tclassName=\"sc-arrow-rim\"\n\t\t\t\t\t\t\td=\"M4 3 L19.6 13.9 L10.2 12.7 L7.3 21.7 Z\"\n\t\t\t\t\t\t\tfill={`url(#${fillId})`}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</svg>\n\t\t\t\t)}\n\t\t\t</div>\n\n\t\t\t{label ? (\n\t\t\t\t<div\n\t\t\t\t\tref={setLabelEl}\n\t\t\t\t\tclassName={idle ? \"sc-label is-idle\" : \"sc-label\"}\n\t\t\t\t\tdata-smooth-cursor-label=\"\"\n\t\t\t\t>\n\t\t\t\t\t<span className=\"sc-label-text\">{label}</span>\n\t\t\t\t</div>\n\t\t\t) : null}\n\t\t</div>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/smooth-cursor/SmoothCursor.tsx"},{"path":"react/src/components/smooth-cursor/index.ts","content":"\"use client\";\n\nexport { SmoothCursor } from \"./SmoothCursor\";\nexport type { SmoothCursorProps, SpringConfig } from \"./SmoothCursor\";\n","type":"registry:ui","target":"@ui/fancy/components/smooth-cursor/index.ts"},{"path":"react/src/components/smooth-cursor/smooth-cursor-core.ts","content":"\"use client\";\n\n/**\n * SmoothCursor core — framework-free spring-physics cursor engine.\n *\n * Owns the rAF loop, the spring integration, rotation-from-velocity, the\n * trailing name label (its own softer spring, tethered to the arrow), the idle\n * timer, the `document`-level pointer listeners and the native-cursor\n * hide/restore. The wrapper owns markup, the cursor snippet, `class`, a11y\n * attributes and the `prefers-reduced-motion` query. The query's *mount* value\n * is forwarded as a creation option (plain assignment, no snap); only a later\n * *change* event is forwarded through `setOptions({ reducedMotion })`, which\n * snaps. That split reproduces the original wrapper's two distinct\n * reduced-motion entry points.\n *\n * The loop sleeps once both springs have settled on the pointer and wakes on\n * the next pointer move; the first frame after a wake is capped to one 60 Hz\n * step so a long rest never turns into a single oversized integration step.\n */\n\nexport interface SpringConfig {\n\t/** Controls how quickly the animation settles (default: 45) */\n\tdamping?: number;\n\t/** Controls the spring stiffness (default: 400) */\n\tstiffness?: number;\n\t/** Controls the virtual mass of the animated object (default: 1) */\n\tmass?: number;\n}\n\nexport interface SmoothCursorElements {\n\t/** The fixed-position element the engine transforms in place. */\n\tcursor: HTMLElement;\n\t/**\n\t * Optional name-label element. It trails the cursor on its own, softer\n\t * spring (see `labelSpring`) and is written as\n\t * `translate3d(x, y, 0) rotate(<tilt>deg)` — the wrapper adds the visual\n\t * offset from the arrow tip with the CSS `translate` property. Can also be\n\t * attached or swapped later with `setLabel()`.\n\t */\n\tlabel?: HTMLElement | null;\n}\n\nexport interface SmoothCursorInitOptions {\n\t/** Called whenever the engine's visibility (pointer on/off screen) changes. */\n\tonVisibleChange?: (visible: boolean) => void;\n\t/**\n\t * Called when the pointer has rested for `idleFade` ms (`true`) and again on\n\t * the next movement (`false`). Never called while `idleFade` is 0.\n\t */\n\tonIdleChange?: (idle: boolean) => void;\n}\n\nexport interface SmoothCursorLiveOptions {\n\t/** Spring physics configuration. */\n\tspringConfig?: SpringConfig;\n\t/**\n\t * Spring for the label. Every field left out is derived from the resolved\n\t * `springConfig`: stiffness × 0.55, damping × 1.1, same mass — a softer,\n\t * more damped follow that lags the arrow by a few frames.\n\t */\n\tlabelSpring?: SpringConfig;\n\t/**\n\t * Rotate the cursor toward its direction of travel. Defaults to `true` here\n\t * (the engine's original behaviour); wrappers that render an upright arrow\n\t * pass `false`, which eases any existing rotation back to 0.\n\t */\n\trotate?: boolean;\n\t/**\n\t * Milliseconds without pointer movement before `onIdleChange(true)` fires.\n\t * 0 (the default) disables idle tracking entirely.\n\t */\n\tidleFade?: number;\n\t/**\n\t * Wrapper-owned `prefers-reduced-motion` result. Passed at creation it is a\n\t * plain initial value; pushed through `setOptions` it additionally stops the\n\t * loop and snaps to the pointer (zeroing velocity), matching the original\n\t * wrapper's media-query `change` handler.\n\t */\n\treducedMotion?: boolean;\n}\n\nexport interface SmoothCursorEngine {\n\tsetOptions(next: Partial<SmoothCursorLiveOptions>): void;\n\t/** Attach, swap or detach (`null`) the label element after creation. */\n\tsetLabel(label: HTMLElement | null): void;\n\tresize(): void;\n\tdestroy(): void;\n}\n\nconst DEFAULT_DAMPING = 45;\nconst DEFAULT_STIFFNESS = 400;\nconst DEFAULT_MASS = 1;\n/** Label spring = arrow spring scaled by these factors (per missing field). */\nconst LABEL_STIFFNESS_SCALE = 0.55;\nconst LABEL_DAMPING_SCALE = 1.1;\n/** Frame-delta clamp, in seconds — caps the spring step after a long stall. */\nconst MAX_DT = 0.064;\n/** The first frame after the loop wakes from sleep integrates at most one 60 Hz step. */\nconst WAKE_DT = 1 / 60;\n/** Below this movement (px) the rotation target is not recomputed. */\nconst MIN_ROTATION_DISTANCE = 0.1;\n/** How quickly the rendered rotation eases toward the movement-direction target. */\nconst ROTATION_EASE = 0.3;\n/** Furthest the label may fall behind the arrow (px) — a leash, not a chase. */\nconst LABEL_TETHER = 24;\n/** Label tilt per px/s of its own horizontal velocity, and the tilt cap (deg). */\nconst LABEL_TILT_PER_VELOCITY = 0.0035;\nconst LABEL_MAX_TILT = 5;\n/** Settle thresholds: position (px), velocity (px/s), rotation (deg). */\nconst SETTLE_DISTANCE = 0.05;\nconst SETTLE_VELOCITY = 1;\nconst SETTLE_ROTATION = 0.05;\n\nexport function resolveConfig(springConfig: SpringConfig | undefined): Required<SpringConfig> {\n\treturn {\n\t\tdamping: springConfig?.damping ?? DEFAULT_DAMPING,\n\t\tstiffness: springConfig?.stiffness ?? DEFAULT_STIFFNESS,\n\t\tmass: springConfig?.mass ?? DEFAULT_MASS,\n\t};\n}\n\n/** The label's spring: explicit fields win, the rest derive from the arrow's. */\nexport function resolveLabelConfig(\n\tlabelSpring: SpringConfig | undefined,\n\tspringConfig: SpringConfig | undefined\n): Required<SpringConfig> {\n\tconst base = resolveConfig(springConfig);\n\treturn {\n\t\tdamping: labelSpring?.damping ?? base.damping * LABEL_DAMPING_SCALE,\n\t\tstiffness: labelSpring?.stiffness ?? base.stiffness * LABEL_STIFFNESS_SCALE,\n\t\tmass: labelSpring?.mass ?? base.mass,\n\t};\n}\n\nexport function createSmoothCursor(\n\tel: SmoothCursorElements,\n\toptions: SmoothCursorInitOptions & SmoothCursorLiveOptions\n): SmoothCursorEngine {\n\tconst { cursor } = el;\n\tlet label: HTMLElement | null = el.label ?? null;\n\tconst onVisibleChange = options.onVisibleChange;\n\tconst onIdleChange = options.onIdleChange;\n\n\tlet springConfig = options.springConfig;\n\tlet labelSpring = options.labelSpring;\n\tlet rotate = options.rotate ?? true;\n\tlet idleFade = Math.max(0, options.idleFade ?? 0);\n\tlet reducedMotion = options.reducedMotion ?? false;\n\n\t// Arrow spring state\n\tlet posX = 0;\n\tlet posY = 0;\n\tlet velX = 0;\n\tlet velY = 0;\n\tlet targetX = 0;\n\tlet targetY = 0;\n\n\t// Label spring state (targets the arrow's rendered position)\n\tlet labelX = 0;\n\tlet labelY = 0;\n\tlet labelVelX = 0;\n\tlet labelVelY = 0;\n\n\t// Rotation state\n\tlet rotation = 0;\n\tlet prevX = 0;\n\tlet prevY = 0;\n\n\tlet visible = false;\n\tlet idle = false;\n\tlet idleTimer: ReturnType<typeof setTimeout> | null = null;\n\tlet rafId: number | null = null;\n\tlet lastTime = 0;\n\t/** The loop stopped itself because everything settled (not because it was hidden). */\n\tlet sleeping = false;\n\t/** Next frame is the first after a sleep: cap its step to WAKE_DT. */\n\tlet waking = false;\n\tlet destroyed = false;\n\n\tfunction setVisible(next: boolean) {\n\t\tif (visible === next) return;\n\t\tvisible = next;\n\t\tonVisibleChange?.(visible);\n\t}\n\n\tfunction setIdle(next: boolean) {\n\t\tif (idle === next) return;\n\t\tidle = next;\n\t\tonIdleChange?.(idle);\n\t}\n\n\tfunction clearIdleTimer() {\n\t\tif (idleTimer !== null) {\n\t\t\tclearTimeout(idleTimer);\n\t\t\tidleTimer = null;\n\t\t}\n\t}\n\n\t/** Movement happened: leave the idle state and restart the countdown. */\n\tfunction markActive() {\n\t\tclearIdleTimer();\n\t\tsetIdle(false);\n\t\tif (idleFade > 0) {\n\t\t\tidleTimer = setTimeout(() => {\n\t\t\t\tidleTimer = null;\n\t\t\t\tsetIdle(true);\n\t\t\t}, idleFade);\n\t\t}\n\t}\n\n\tfunction startAnimation() {\n\t\tif (rafId === null) {\n\t\t\tif (sleeping) {\n\t\t\t\t// Keep lastTime: the wake frame integrates a real (capped) step\n\t\t\t\t// instead of burning a frame on the timestamp sentinel.\n\t\t\t\twaking = true;\n\t\t\t} else {\n\t\t\t\tlastTime = 0;\n\t\t\t}\n\t\t\tsleeping = false;\n\t\t\trafId = requestAnimationFrame(animate);\n\t\t}\n\t}\n\n\tfunction stopAnimation() {\n\t\tsleeping = false;\n\t\twaking = false;\n\t\tif (rafId !== null) {\n\t\t\tcancelAnimationFrame(rafId);\n\t\t\trafId = null;\n\t\t}\n\t}\n\n\tfunction writeLabel(tilt: number) {\n\t\tif (!label) return;\n\t\tlabel.style.transform =\n\t\t\ttilt === 0\n\t\t\t\t? `translate3d(${labelX}px, ${labelY}px, 0)`\n\t\t\t\t: `translate3d(${labelX}px, ${labelY}px, 0) rotate(${tilt}deg)`;\n\t}\n\n\tfunction snapLabel() {\n\t\tlabelX = posX;\n\t\tlabelY = posY;\n\t\tlabelVelX = 0;\n\t\tlabelVelY = 0;\n\t\twriteLabel(0);\n\t}\n\n\tfunction snapToTarget() {\n\t\tposX = targetX;\n\t\tposY = targetY;\n\t\tvelX = 0;\n\t\tvelY = 0;\n\t\tif (!rotate) rotation = 0;\n\t\tcursor.style.transform = `translate3d(${posX}px, ${posY}px, 0)`;\n\t\tsnapLabel();\n\t}\n\n\t/** Pointer (re)appears: jump everything to it without a fly-in. */\n\tfunction placeAtTarget() {\n\t\tposX = targetX;\n\t\tposY = targetY;\n\t\tprevX = targetX;\n\t\tprevY = targetY;\n\t\tlabelX = targetX;\n\t\tlabelY = targetY;\n\t\tlabelVelX = 0;\n\t\tlabelVelY = 0;\n\t}\n\n\tfunction onMouseMove(e: MouseEvent) {\n\t\ttargetX = e.clientX;\n\t\ttargetY = e.clientY;\n\n\t\tif (!visible) {\n\t\t\tplaceAtTarget();\n\t\t\tsetVisible(true);\n\t\t}\n\n\t\tmarkActive();\n\n\t\tif (reducedMotion) {\n\t\t\tsnapToTarget();\n\t\t} else {\n\t\t\tstartAnimation();\n\t\t}\n\t}\n\n\tfunction onMouseLeave() {\n\t\tsetVisible(false);\n\t\tclearIdleTimer();\n\t\tsetIdle(false);\n\t\tstopAnimation();\n\t}\n\n\tfunction onMouseEnter(e: MouseEvent) {\n\t\ttargetX = e.clientX;\n\t\ttargetY = e.clientY;\n\t\tplaceAtTarget();\n\t\tsetVisible(true);\n\t\tmarkActive();\n\n\t\tif (!reducedMotion) {\n\t\t\tstartAnimation();\n\t\t} else {\n\t\t\tsnapToTarget();\n\t\t}\n\t}\n\n\tfunction animate(time: number) {\n\t\trafId = null;\n\t\tif (!visible) return;\n\n\t\tif (lastTime === 0) {\n\t\t\tlastTime = time;\n\t\t\trafId = requestAnimationFrame(animate);\n\t\t\treturn;\n\t\t}\n\n\t\tlet dt = Math.min((time - lastTime) / 1000, MAX_DT);\n\t\tif (waking) {\n\t\t\tdt = Math.min(dt, WAKE_DT);\n\t\t\twaking = false;\n\t\t}\n\t\tlastTime = time;\n\n\t\tconst { stiffness, damping, mass } = resolveConfig(springConfig);\n\n\t\t// Spring physics: F = -k * displacement - c * velocity\n\t\tconst forceX = -stiffness * (posX - targetX) - damping * velX;\n\t\tconst forceY = -stiffness * (posY - targetY) - damping * velY;\n\n\t\tconst accX = forceX / mass;\n\t\tconst accY = forceY / mass;\n\n\t\tvelX += accX * dt;\n\t\tvelY += accY * dt;\n\n\t\tposX += velX * dt;\n\t\tposY += velY * dt;\n\n\t\tif (rotate) {\n\t\t\t// Calculate rotation based on movement direction\n\t\t\tconst dx = posX - prevX;\n\t\t\tconst dy = posY - prevY;\n\t\t\tconst distance = Math.sqrt(dx * dx + dy * dy);\n\n\t\t\tif (distance > MIN_ROTATION_DISTANCE) {\n\t\t\t\tconst targetRotation = Math.atan2(dy, dx) * (180 / Math.PI) + 90;\n\t\t\t\tlet diff = targetRotation - rotation;\n\t\t\t\twhile (diff > 180) diff -= 360;\n\t\t\t\twhile (diff < -180) diff += 360;\n\t\t\t\trotation += diff * ROTATION_EASE;\n\t\t\t}\n\t\t} else if (rotation !== 0) {\n\t\t\t// Upright mode: ease any leftover angle home.\n\t\t\trotation += -rotation * ROTATION_EASE;\n\t\t\tif (Math.abs(rotation) < SETTLE_ROTATION) rotation = 0;\n\t\t}\n\n\t\tprevX = posX;\n\t\tprevY = posY;\n\n\t\t// Label: its own softer spring chasing the arrow, held on a leash.\n\t\tlet tilt = 0;\n\t\tlet labelSettled = true;\n\t\tif (label) {\n\t\t\tconst lc = resolveLabelConfig(labelSpring, springConfig);\n\t\t\tconst lfx = -lc.stiffness * (labelX - posX) - lc.damping * labelVelX;\n\t\t\tconst lfy = -lc.stiffness * (labelY - posY) - lc.damping * labelVelY;\n\t\t\tlabelVelX += (lfx / lc.mass) * dt;\n\t\t\tlabelVelY += (lfy / lc.mass) * dt;\n\t\t\tlabelX += labelVelX * dt;\n\t\t\tlabelY += labelVelY * dt;\n\n\t\t\tconst gapX = labelX - posX;\n\t\t\tconst gapY = labelY - posY;\n\t\t\tconst gap = Math.sqrt(gapX * gapX + gapY * gapY);\n\t\t\tif (gap > LABEL_TETHER) {\n\t\t\t\tconst k = LABEL_TETHER / gap;\n\t\t\t\tlabelX = posX + gapX * k;\n\t\t\t\tlabelY = posY + gapY * k;\n\t\t\t}\n\n\t\t\ttilt = Math.max(\n\t\t\t\t-LABEL_MAX_TILT,\n\t\t\t\tMath.min(LABEL_MAX_TILT, labelVelX * LABEL_TILT_PER_VELOCITY)\n\t\t\t);\n\t\t\tif (Math.abs(tilt) < 0.01) tilt = 0;\n\n\t\t\tlabelSettled =\n\t\t\t\tMath.abs(labelX - targetX) < SETTLE_DISTANCE &&\n\t\t\t\tMath.abs(labelY - targetY) < SETTLE_DISTANCE &&\n\t\t\t\tMath.abs(labelVelX) < SETTLE_VELOCITY &&\n\t\t\t\tMath.abs(labelVelY) < SETTLE_VELOCITY;\n\t\t}\n\n\t\tconst settled =\n\t\t\tlabelSettled &&\n\t\t\tMath.abs(posX - targetX) < SETTLE_DISTANCE &&\n\t\t\tMath.abs(posY - targetY) < SETTLE_DISTANCE &&\n\t\t\tMath.abs(velX) < SETTLE_VELOCITY &&\n\t\t\tMath.abs(velY) < SETTLE_VELOCITY &&\n\t\t\t(rotate || rotation === 0);\n\n\t\tif (settled) {\n\t\t\t// Land exactly on the pointer and let the loop sleep until the next move.\n\t\t\tposX = targetX;\n\t\t\tposY = targetY;\n\t\t\tvelX = 0;\n\t\t\tvelY = 0;\n\t\t\tprevX = posX;\n\t\t\tprevY = posY;\n\t\t\tif (label) {\n\t\t\t\tlabelX = targetX;\n\t\t\t\tlabelY = targetY;\n\t\t\t\tlabelVelX = 0;\n\t\t\t\tlabelVelY = 0;\n\t\t\t\ttilt = 0;\n\t\t\t}\n\t\t}\n\n\t\tcursor.style.transform = `translate3d(${posX}px, ${posY}px, 0) rotate(${rotation}deg)`;\n\t\twriteLabel(tilt);\n\n\t\tif (settled) {\n\t\t\tsleeping = true;\n\t\t\treturn;\n\t\t}\n\n\t\trafId = requestAnimationFrame(animate);\n\t}\n\n\t// Capture the host's own inline cursor so teardown restores it exactly\n\t// instead of wiping it (e.g. an app-set `crosshair`).\n\tconst previousBodyCursor = document.body.style.cursor;\n\tdocument.body.style.cursor = \"none\";\n\n\tdocument.addEventListener(\"mousemove\", onMouseMove);\n\tdocument.documentElement.addEventListener(\"mouseleave\", onMouseLeave);\n\tdocument.documentElement.addEventListener(\"mouseenter\", onMouseEnter);\n\n\treturn {\n\t\tsetOptions(next) {\n\t\t\tif (destroyed) return;\n\t\t\tif (\"springConfig\" in next) {\n\t\t\t\tspringConfig = next.springConfig;\n\t\t\t}\n\t\t\tif (\"labelSpring\" in next) {\n\t\t\t\tlabelSpring = next.labelSpring;\n\t\t\t}\n\t\t\tif (\"rotate\" in next) {\n\t\t\t\trotate = next.rotate ?? true;\n\t\t\t\t// Upright again: ease the leftover angle home (or snap it).\n\t\t\t\tif (!rotate && rotation !== 0) {\n\t\t\t\t\tif (reducedMotion) {\n\t\t\t\t\t\trotation = 0;\n\t\t\t\t\t\tcursor.style.transform = `translate3d(${posX}px, ${posY}px, 0)`;\n\t\t\t\t\t} else if (visible) {\n\t\t\t\t\t\tstartAnimation();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (\"idleFade\" in next) {\n\t\t\t\tidleFade = Math.max(0, next.idleFade ?? 0);\n\t\t\t\tclearIdleTimer();\n\t\t\t\tif (idleFade === 0) {\n\t\t\t\t\tsetIdle(false);\n\t\t\t\t} else if (visible && !idle) {\n\t\t\t\t\tmarkActive();\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (\"reducedMotion\" in next) {\n\t\t\t\t// Mirrors the original wrapper's `onMotionChange` exactly: the snap\n\t\t\t\t// is UNCONDITIONAL (it is the only path that zeroes velX/velY), and\n\t\t\t\t// only the resume is gated on `visible`. The mount-time value is\n\t\t\t\t// NOT routed here — the wrapper passes it as a creation option, the\n\t\t\t\t// way the original's plain `reducedMotion = motionQuery.matches`\n\t\t\t\t// assignment did, so this branch is only ever reached from a real\n\t\t\t\t// media-query change event.\n\t\t\t\treducedMotion = next.reducedMotion ?? false;\n\t\t\t\tif (reducedMotion) {\n\t\t\t\t\tstopAnimation();\n\t\t\t\t\tsnapToTarget();\n\t\t\t\t} else if (visible) {\n\t\t\t\t\tstartAnimation();\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\tsetLabel(next) {\n\t\t\tif (destroyed || next === label) return;\n\t\t\tlabel = next;\n\t\t\tif (!label) return;\n\t\t\t// A fresh label starts glued to the arrow, then trails from there.\n\t\t\tsnapLabel();\n\t\t},\n\t\tresize() {\n\t\t\t// No geometry to recompute: the cursor tracks raw pointer coordinates.\n\t\t},\n\t\tdestroy() {\n\t\t\tif (destroyed) return;\n\t\t\tdestroyed = true;\n\t\t\tdocument.body.style.cursor = previousBodyCursor;\n\t\t\tdocument.removeEventListener(\"mousemove\", onMouseMove);\n\t\t\tdocument.documentElement.removeEventListener(\"mouseleave\", onMouseLeave);\n\t\t\tdocument.documentElement.removeEventListener(\"mouseenter\", onMouseEnter);\n\t\t\tclearIdleTimer();\n\t\t\tstopAnimation();\n\t\t},\n\t};\n}\n","type":"registry:ui","target":"@ui/fancy/components/smooth-cursor/smooth-cursor-core.ts"},{"path":"react/src/components/smooth-cursor/smooth-cursor.css","content":"/*\n * SmoothCursor — the Svelte source's scoped <style> block. Plain CSS is global,\n * so every rule is anchored on the component's own root class `.smooth-cursor`\n * (the Svelte root already carries it). Class names and custom properties are\n * identical to the Svelte source.\n */\n\n.smooth-cursor {\n\t--_sc-hot: color-mix(in oklab, var(--_sc-color) 45%, white);\n\t--_sc-deep: color-mix(in oklab, var(--_sc-color) 82%, black);\n\t--_sc-ink: var(--smooth-cursor-label-color, #fff);\n}\n\n.smooth-cursor .sc-pointer,\n.smooth-cursor .sc-label {\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\twill-change: transform;\n}\n\n/* The arrow always paints over the pill when the pill catches up behind it. */\n.smooth-cursor .sc-pointer {\n\tz-index: 1;\n}\n\n.smooth-cursor .sc-arrow {\n\tdisplay: block;\n\toverflow: visible;\n\tfilter: drop-shadow(0 1px 1.25px rgb(0 0 0 / 0.32))\n\t\tdrop-shadow(0 4px 9px color-mix(in oklab, var(--_sc-color) 38%, transparent));\n}\n\n/* Rotating mode: turn the arrowhead so it points \"up\" — the engine's 0deg. */\n.smooth-cursor .sc-pointer--rotate .sc-arrow {\n\trotate: 32.5deg;\n}\n\n.smooth-cursor .sc-arrow-rim {\n\tstroke: #fff;\n\tstroke-width: 3;\n\tstroke-linejoin: round;\n\tpaint-order: stroke fill;\n}\n\n.smooth-cursor .sc-stop-hot {\n\tstop-color: var(--_sc-hot);\n}\n.smooth-cursor .sc-stop-body {\n\tstop-color: var(--_sc-color);\n}\n.smooth-cursor .sc-stop-deep {\n\tstop-color: var(--_sc-deep);\n}\n\n/* Name pill: tucked under the arrow's back edge, square-ish corner pointing at the tip. */\n.smooth-cursor .sc-label {\n\ttranslate: 13px 17px;\n\ttransform-origin: 0 0;\n\tdisplay: flex;\n\talign-items: center;\n\tmax-width: 180px;\n\theight: 22px;\n\tpadding: 0 9px;\n\tborder-radius: 3px 11px 11px 11px;\n\tbackground: linear-gradient(\n\t\t180deg,\n\t\tcolor-mix(in oklab, var(--_sc-color) 86%, white) 0%,\n\t\tvar(--_sc-color) 55%,\n\t\tcolor-mix(in oklab, var(--_sc-color) 92%, black) 100%\n\t);\n\tcolor: var(--_sc-ink);\n\tbox-shadow:\n\t\t0 0 0 1px rgb(255 255 255 / 0.92),\n\t\tinset 0 1px 0 rgb(255 255 255 / 0.28),\n\t\t0 1px 2px rgb(0 0 0 / 0.28),\n\t\t0 6px 16px -4px color-mix(in oklab, var(--_sc-color) 45%, transparent);\n\topacity: 1;\n}\n\n.smooth-cursor .sc-label-text {\n\toverflow: hidden;\n\tfont-size: 12px;\n\tfont-weight: 500;\n\tline-height: 1;\n\tletter-spacing: 0.01em;\n\twhite-space: nowrap;\n\ttext-overflow: ellipsis;\n\ttext-shadow: 0 1px 0 rgb(0 0 0 / 0.14);\n}\n\n.smooth-cursor .sc-label.is-idle {\n\topacity: 0.35;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n\t.smooth-cursor .sc-label {\n\t\ttransition: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/smooth-cursor/smooth-cursor.css"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/smooth-cursor?framework=react"}}