{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "effect-03",
  "title": "Named cursor",
  "description": "The pointer drawn inside an area on a light spring: an arrow with the name of its owner, a dot that grows into the word of what it is over, or a crosshair with its coordinates; fields keep the cursor of the system.",
  "dependencies": [],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/agent-run.json",
    "https://ui.ndekocode.dev/r/avatar.json",
    "https://ui.ndekocode.dev/r/chat-motion.json",
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/input.json",
    "https://ui.ndekocode.dev/r/sample-scenes.json",
    "https://ui.ndekocode.dev/r/segments.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-autoplay.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/effect/effect-03.tsx",
      "content": "'use client';\n\nimport {\n  useEffect,\n  useImperativeHandle,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type ReactNode,\n} from 'react';\n\nimport {\n  useAutoplay,\n  type AutoplayStep,\n} from '@/registry/ndeko/hooks/use-autoplay';\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { seriesColorOf } from '@/registry/ndeko/lib/agent-run';\nimport { FileTextIcon } from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Avatar } from '@/registry/ndeko/ui/avatar';\nimport { Swap } from '@/registry/ndeko/ui/chat-motion';\nimport { Input } from '@/registry/ndeko/ui/input';\nimport { Segments, type Segment } from '@/registry/ndeko/ui/segments';\n\nimport { createFollower, type Follower } from './_parts/follower';\nimport { hundredthsOf } from './_parts/rounded';\nimport { useHover } from './_parts/use-hover';\n\nexport type CursorMode = 'name' | 'label' | 'crosshair';\n\ntype Presence = {\n  /** The cursor is inside the area. */\n  isInside: boolean;\n  /** The word a target under the cursor gives with `data-cursor-label`. */\n  label: string | null;\n  /** The cursor is over a field, or over an element that asks for the cursor of the system. */\n  isNative: boolean;\n};\n\ntype CursorParts = {\n  point: HTMLElement;\n  column: HTMLElement;\n  row: HTMLElement;\n  across: HTMLElement;\n  down: HTMLElement;\n};\n\ntype AreaSize = {\n  width: number;\n  height: number;\n};\n\ntype DotState = 'hidden' | 'shown' | 'grown';\n\ntype InkStyle = React.CSSProperties & { '--cursor-ink': string };\n\nconst AWAY: Presence = { isInside: false, label: null, isNative: false };\n\nconst FINE_POINTER_QUERY = '(pointer: fine)';\n\nconst NATIVE_CURSOR_TARGETS =\n  'input, textarea, select, [contenteditable=\"\"], [contenteditable=\"true\"], [data-cursor=\"native\"]';\n\nconst LABELLED_TARGET = '[data-cursor-label]';\n\n/** A pointer is trailed by a light spring, a demonstration glides from place to place. */\nconst POINTER_STIFFNESS = 650;\n\nconst DEMO_STIFFNESS = 70;\n\nconst PRECISION_PX = 0.2;\n\n/** A gliding cursor is close enough to its place this long after it set off to tell what it is over. */\nconst ARRIVAL_MS = 420;\n\n/** Near the right or the bottom edge, the name and the figures move to the other side of the cursor so the edge does not cut them. */\nconst FLIP_ACROSS_PX = 128;\n\nconst FLIP_DOWN_PX = 44;\n\n/** The tip of the arrow sits on the point of the cursor. */\nconst ARROW_PATH = 'M0 0v15.2l4-3.9 2.6 5.9 2.7-1.2-2.6-5.8h5.6Z';\n\nfunction subscribeToPointerKind(onChange: () => void) {\n  const media = window.matchMedia(FINE_POINTER_QUERY);\n  media.addEventListener('change', onChange);\n  return () => media.removeEventListener('change', onChange);\n}\n\nfunction readFinePointer() {\n  return window.matchMedia(FINE_POINTER_QUERY).matches;\n}\n\nfunction fineOnServer() {\n  return false;\n}\n\n/** Whether the main pointer of the device aims finely, as a mouse or a trackpad does. Read the same way as reduced motion, so the first render matches the server. */\nfunction useFinePointer() {\n  return useSyncExternalStore(\n    subscribeToPointerKind,\n    readFinePointer,\n    fineOnServer\n  );\n}\n\nfunction labelOf(target: Element, area: HTMLElement) {\n  const labelled = target.closest(LABELLED_TARGET);\n  if (!labelled) {\n    return null;\n  }\n  if (!area.contains(labelled)) {\n    return null;\n  }\n  return labelled.getAttribute('data-cursor-label');\n}\n\nfunction isOverNativeTarget(target: Element, area: HTMLElement) {\n  const native = target.closest(NATIVE_CURSOR_TARGETS);\n  if (!native) {\n    return false;\n  }\n  return area.contains(native);\n}\n\nfunction presenceOf(target: EventTarget | null, area: HTMLElement): Presence {\n  if (!(target instanceof Element)) {\n    return { isInside: true, label: null, isNative: false };\n  }\n  if (!area.contains(target)) {\n    return AWAY;\n  }\n  const label = labelOf(target, area);\n  const isNative = isOverNativeTarget(target, area);\n  return { isInside: true, label, isNative };\n}\n\n/** What lies under a point of the area. The drawn cursor lets every pointer through, so it is never the answer. */\nfunction presenceAt(area: HTMLElement, x: number, y: number) {\n  const box = area.getBoundingClientRect();\n  const target = document.elementFromPoint(box.left + x, box.top + y);\n  return presenceOf(target, area);\n}\n\nfunction isSamePresence(first: Presence, second: Presence) {\n  const isSameSide = first.isInside === second.isInside;\n  const isSameLabel = first.label === second.label;\n  const isSameKind = first.isNative === second.isNative;\n  return isSameSide && isSameLabel && isSameKind;\n}\n\nfunction keptOrNext(current: Presence, next: Presence) {\n  if (isSamePresence(current, next)) {\n    return current;\n  }\n  return next;\n}\n\nfunction writeFigure(element: HTMLElement, value: number) {\n  const text = String(Math.round(value));\n  if (element.textContent !== text) {\n    element.textContent = text;\n  }\n}\n\nfunction writeFlag(element: HTMLElement, name: string, isOn: boolean) {\n  const value = String(isOn);\n  if (element.dataset[name] !== value) {\n    element.dataset[name] = value;\n  }\n}\n\nfunction drawCursor(\n  parts: CursorParts,\n  values: readonly number[],\n  size: AreaSize\n) {\n  const [x, y] = values;\n  const left = hundredthsOf(x);\n  const top = hundredthsOf(y);\n  parts.point.style.transform = `translate3d(${left}px, ${top}px, 0)`;\n  parts.column.style.transform = `translate3d(${left}px, 0, 0)`;\n  parts.row.style.transform = `translate3d(0, ${top}px, 0)`;\n  writeFigure(parts.across, x);\n  writeFigure(parts.down, y);\n  const isNearRight = x > size.width - FLIP_ACROSS_PX;\n  const isNearBottom = y > size.height - FLIP_DOWN_PX;\n  writeFlag(parts.point, 'flipAcross', isNearRight);\n  writeFlag(parts.point, 'flipDown', isNearBottom);\n}\n\nfunction cursorKindOf(followsPointer: boolean, presence: Presence) {\n  const isDrawn = presence.isInside && !presence.isNative;\n  if (followsPointer && isDrawn) {\n    return 'drawn';\n  }\n  return 'system';\n}\n\nfunction dotStateOf(isShown: boolean, hasLabel: boolean): DotState {\n  if (!isShown) {\n    return 'hidden';\n  }\n  if (hasLabel) {\n    return 'grown';\n  }\n  return 'shown';\n}\n\nexport type CursorAreaHandle = {\n  /** Glides the drawn cursor to a place of the area, in shares of its width and height, for a demonstration. */\n  glideTo: (across: number, down: number) => void;\n  /** Hides the drawn cursor, as a pointer that left the area. */\n  leave: () => void;\n};\n\nexport type CursorAreaProps = {\n  mode: CursorMode;\n  /** Whose arrow it is, in the name mode. Their picture is drawn from the name. */\n  name: string;\n  /** The colour of the arrow, such as the series colour of the person. */\n  color?: string;\n  children: ReactNode;\n  /** Moves the drawn cursor from outside, for a demonstration. */\n  handleRef?: React.Ref<CursorAreaHandle>;\n  className?: string;\n};\n\n/**\n * An area where the pointer is drawn: an arrow with a name, a dot that grows into the word of a\n * `data-cursor-label` target, or a crosshair with its coordinates. It trails the pointer on a light\n * spring. The system cursor stays over fields and `data-cursor='native'`, and everywhere on touch\n * screens, coarse pointers and under reduced motion.\n */\nexport function CursorArea({\n  mode,\n  name,\n  color = 'var(--primary)',\n  children,\n  handleRef,\n  className,\n}: CursorAreaProps) {\n  const areaRef = useRef<HTMLDivElement>(null);\n  const pointRef = useRef<HTMLSpanElement>(null);\n  const columnRef = useRef<HTMLSpanElement>(null);\n  const rowRef = useRef<HTMLSpanElement>(null);\n  const acrossRef = useRef<HTMLSpanElement>(null);\n  const downRef = useRef<HTMLSpanElement>(null);\n  const follower = useRef<Follower | null>(null);\n  const arrival = useRef<number | undefined>(undefined);\n  const [presence, setPresence] = useState<Presence>(AWAY);\n  const [lastLabel, setLastLabel] = useState('');\n  const isStill = usePrefersReducedMotion();\n  const isFinePointer = useFinePointer();\n  const followsPointer = isFinePointer && !isStill;\n\n  useEffect(() => {\n    const area = areaRef.current;\n    const point = pointRef.current;\n    const column = columnRef.current;\n    const row = rowRef.current;\n    const across = acrossRef.current;\n    const down = downRef.current;\n    const hasArea = area !== null;\n    const hasPoint = point !== null;\n    const hasLines = column !== null && row !== null;\n    const hasFigures = across !== null && down !== null;\n    const hasParts = hasArea && hasPoint && hasLines && hasFigures;\n    if (!hasParts) {\n      return;\n    }\n    const parts: CursorParts = { point, column, row, across, down };\n    const size: AreaSize = {\n      width: area.clientWidth,\n      height: area.clientHeight,\n    };\n    const observer = new ResizeObserver(() => {\n      size.width = area.clientWidth;\n      size.height = area.clientHeight;\n    });\n    observer.observe(area);\n    const created = createFollower(\n      2,\n      (values) => drawCursor(parts, values, size),\n      { stiffness: POINTER_STIFFNESS, precision: PRECISION_PX }\n    );\n    follower.current = created;\n    return () => {\n      created.stop();\n      observer.disconnect();\n      window.clearTimeout(arrival.current);\n      follower.current = null;\n    };\n  }, []);\n\n  function show(next: Presence) {\n    if (next.label !== null) {\n      setLastLabel(next.label);\n    }\n    setPresence((current) => keptOrNext(current, next));\n  }\n\n  function moveTo(x: number, y: number, stiffness: number) {\n    const current = follower.current;\n    if (!current) {\n      return;\n    }\n    if (!presence.isInside) {\n      current.place([x, y]);\n      return;\n    }\n    current.setStiffness(stiffness);\n    current.follow([x, y]);\n  }\n\n  function leave() {\n    window.clearTimeout(arrival.current);\n    show(AWAY);\n  }\n\n  function followPointer(event: React.PointerEvent<HTMLDivElement>) {\n    if (!followsPointer) {\n      return;\n    }\n    if (event.pointerType === 'touch') {\n      leave();\n      return;\n    }\n    window.clearTimeout(arrival.current);\n    const box = event.currentTarget.getBoundingClientRect();\n    const x = event.clientX - box.left;\n    const y = event.clientY - box.top;\n    moveTo(x, y, POINTER_STIFFNESS);\n    show(presenceOf(event.target, event.currentTarget));\n  }\n\n  function leaveWithPointer() {\n    if (followsPointer) {\n      leave();\n    }\n  }\n\n  function hideUnderFinger(event: React.PointerEvent<HTMLDivElement>) {\n    const isFinger = event.pointerType === 'touch';\n    if (followsPointer && isFinger) {\n      leave();\n    }\n  }\n\n  useImperativeHandle(handleRef, () => ({\n    glideTo(acrossShare: number, downShare: number) {\n      const area = areaRef.current;\n      if (!area) {\n        return;\n      }\n      window.clearTimeout(arrival.current);\n      const x = acrossShare * area.clientWidth;\n      const y = downShare * area.clientHeight;\n      if (isStill) {\n        follower.current?.place([x, y]);\n        show(presenceAt(area, x, y));\n        return;\n      }\n      moveTo(x, y, DEMO_STIFFNESS);\n      show({ isInside: true, label: null, isNative: false });\n      arrival.current = window.setTimeout(() => {\n        show(presenceAt(area, x, y));\n      }, ARRIVAL_MS);\n    },\n    leave,\n  }));\n\n  const isShown = presence.isInside && !presence.isNative;\n  const hasLabel = presence.label !== null;\n  const showsArrow = isShown && mode === 'name';\n  const showsDot = isShown && mode === 'label';\n  const showsLabel = showsDot && hasLabel;\n  const showsCrosshair = isShown && mode === 'crosshair';\n  const inkStyle: InkStyle = { '--cursor-ink': color };\n\n  return (\n    <div\n      ref={areaRef}\n      data-cursor={cursorKindOf(followsPointer, presence)}\n      onPointerMove={followPointer}\n      onPointerLeave={leaveWithPointer}\n      onPointerDown={hideUnderFinger}\n      className={cn(\n        'relative isolate data-[cursor=drawn]:cursor-none data-[cursor=drawn]:**:cursor-none',\n        className\n      )}\n    >\n      {children}\n      <div\n        aria-hidden='true'\n        style={inkStyle}\n        className='pointer-events-none absolute inset-0 z-10 overflow-hidden rounded-[inherit]'\n      >\n        <span\n          ref={columnRef}\n          data-visible={showsCrosshair}\n          className='bg-primary/45 absolute inset-y-0 left-0 w-px opacity-0 transition-opacity duration-150 data-[visible=true]:opacity-100'\n        />\n        <span\n          ref={rowRef}\n          data-visible={showsCrosshair}\n          className='bg-primary/45 absolute inset-x-0 top-0 h-px opacity-0 transition-opacity duration-150 data-[visible=true]:opacity-100'\n        />\n        <span ref={pointRef} className='group/point absolute top-0 left-0'>\n          <span\n            data-visible={showsArrow}\n            className='absolute top-0 left-0 opacity-0 transition-opacity duration-150 data-[visible=true]:opacity-100'\n          >\n            <svg\n              width='16'\n              height='21'\n              viewBox='-1 -1 16 21'\n              className='absolute -top-px -left-px'\n            >\n              <path\n                d={ARROW_PATH}\n                fill='var(--cursor-ink)'\n                stroke='white'\n                strokeWidth='1.25'\n                strokeLinejoin='round'\n              />\n            </svg>\n            <span className='surface-raised absolute top-[18px] left-3 flex h-6 items-center gap-1.5 rounded-full ps-0.5 pe-2 text-xs font-medium whitespace-nowrap group-data-[flip-across=true]/point:-translate-x-[calc(100%+1.25rem)] group-data-[flip-down=true]/point:-translate-y-[calc(100%+1.375rem)]'>\n              <Avatar seed={name} className='size-5' />\n              {name}\n            </span>\n          </span>\n          <span\n            data-dot={dotStateOf(showsDot, hasLabel)}\n            className='bg-foreground absolute size-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full opacity-0 transition-[opacity,scale] duration-150 ease-(--ease-out-strong) data-[dot=grown]:scale-[2.6] data-[dot=shown]:opacity-100'\n          />\n          <span\n            data-visible={showsLabel}\n            className='bg-foreground text-background absolute flex h-7 -translate-x-1/2 -translate-y-1/2 scale-75 items-center rounded-full px-3 text-xs font-medium whitespace-nowrap opacity-0 transition-[opacity,scale] duration-150 ease-(--ease-out-strong) data-[visible=true]:scale-100 data-[visible=true]:opacity-100'\n          >\n            <Swap swapKey={lastLabel}>{lastLabel}</Swap>\n          </span>\n          <span\n            data-visible={showsCrosshair}\n            className='bg-primary absolute size-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full opacity-0 transition-opacity duration-150 data-[visible=true]:opacity-100'\n          />\n          <span\n            data-visible={showsCrosshair}\n            className='surface-raised absolute top-2.5 left-2.5 flex h-5 items-center gap-1 rounded-md px-1.5 text-[11px] whitespace-nowrap tabular-nums opacity-0 transition-opacity duration-150 group-data-[flip-across=true]/point:-translate-x-[calc(100%+1.25rem)] group-data-[flip-down=true]/point:-translate-y-[calc(100%+1.25rem)] data-[visible=true]:opacity-100'\n          >\n            <span className='text-muted-foreground'>x</span>\n            <span ref={acrossRef} className='min-w-[3ch] text-end' />\n            <span className='text-muted-foreground ms-1'>y</span>\n            <span ref={downRef} className='min-w-[3ch] text-end' />\n          </span>\n        </span>\n      </div>\n    </div>\n  );\n}\n\ntype BoardTile = {\n  id: string;\n  name: string;\n  /** The word the cursor grows into over the tile: what a click does. */\n  action: string;\n  picture?: string;\n};\n\ntype Waypoint = {\n  across: number;\n  down: number;\n};\n\n/** One beat of the demonstration: a change of mode, a glide to a place, or the cursor leaving the area. */\ntype DemoBeat =\n  | { after: number; kind: 'mode'; mode: CursorMode }\n  | { after: number; kind: 'glide'; place: Waypoint }\n  | { after: number; kind: 'leave' };\n\nconst TILES: BoardTile[] = [\n  {\n    id: 'moodboard',\n    name: 'Moodboard',\n    action: 'Select',\n    picture: '/images/scenes/scene-06.svg',\n  },\n  { id: 'brief', name: 'Launch brief', action: 'Select' },\n];\n\nconst MODES: Segment<CursorMode>[] = [\n  { value: 'name', label: 'Name' },\n  { value: 'label', label: 'Label' },\n  { value: 'crosshair', label: 'Crosshair' },\n];\n\nconst ROUND_MODES: CursorMode[] = ['name', 'label', 'crosshair'];\n\nconst ENTRY: Waypoint = { across: 0.12, down: 0.18 };\n\nconst OVER_PICTURE: Waypoint = { across: 0.28, down: 0.38 };\n\nconst OVER_BRIEF: Waypoint = { across: 0.73, down: 0.3 };\n\nconst OVER_FIELD: Waypoint = { across: 0.42, down: 0.87 };\n\nconst ON_THE_WAY_OUT: Waypoint = { across: 0.62, down: 0.62 };\n\n/** Where the arrow rests under reduced motion: over the brief, as a colleague looking at it. */\nconst STILL_AT: Waypoint = { across: 0.66, down: 0.34 };\n\nfunction roundOf(mode: CursorMode): DemoBeat[] {\n  return [\n    { after: 700, kind: 'mode', mode },\n    { after: 300, kind: 'glide', place: ENTRY },\n    { after: 900, kind: 'glide', place: OVER_PICTURE },\n    { after: 1300, kind: 'glide', place: OVER_BRIEF },\n    { after: 1300, kind: 'glide', place: OVER_FIELD },\n    { after: 1300, kind: 'glide', place: ON_THE_WAY_OUT },\n    { after: 1100, kind: 'leave' },\n  ];\n}\n\n/** The same path in each mode: in from the corner, over the picture, over the brief, over the field, and out. */\nconst DEMO_BEATS: DemoBeat[] = ROUND_MODES.flatMap(roundOf);\n\nfunction TileFace({ tile }: { tile: BoardTile }) {\n  if (tile.picture) {\n    return (\n      <span\n        aria-hidden='true'\n        style={{ backgroundImage: `url(${tile.picture})` }}\n        className='block aspect-[4/3] rounded-md bg-cover bg-center'\n      />\n    );\n  }\n  return (\n    <span\n      aria-hidden='true'\n      className='bg-muted text-muted-foreground flex aspect-[4/3] items-center justify-center rounded-md'\n    >\n      <FileTextIcon className='size-5' />\n    </span>\n  );\n}\n\ntype BoardTileButtonProps = {\n  tile: BoardTile;\n  isSelected: boolean;\n  onToggle: () => void;\n};\n\nfunction BoardTileButton({ tile, isSelected, onToggle }: BoardTileButtonProps) {\n  return (\n    <button\n      type='button'\n      aria-pressed={isSelected}\n      data-cursor-label={tile.action}\n      onClick={onToggle}\n      className='bg-card hairline-ring ring-border aria-pressed:ring-primary focus-visible:ring-ring focus-visible:ring-offset-background flex min-w-0 cursor-pointer flex-col gap-1.5 rounded-lg p-1.5 text-start shadow-[0_1px_2px_rgb(0_0_0/0.04)] transition-[scale,box-shadow] duration-150 ease-(--ease-out-strong) outline-none focus-visible:ring-2 focus-visible:ring-offset-2 active:scale-[0.97] aria-pressed:ring-[1.5px]'\n    >\n      <TileFace tile={tile} />\n      <span className='truncate px-0.5 text-xs font-medium'>{tile.name}</span>\n    </button>\n  );\n}\n\nfunction toggledSelection(current: string | null, id: string) {\n  if (current === id) {\n    return null;\n  }\n  return id;\n}\n\nexport default function Effect03() {\n  const [mode, setMode] = useState<CursorMode>('name');\n  const [selectedId, setSelectedId] = useState<string | null>(null);\n  const area = useRef<CursorAreaHandle>(null);\n  const isStill = usePrefersReducedMotion();\n  const { isHovered, hoverHandlers } = useHover();\n  const steps: AutoplayStep[] = DEMO_BEATS.map((beat) => ({\n    after: beat.after,\n    run: () => playBeat(beat),\n  }));\n  const { ref, isAutoplaying } = useAutoplay(steps, {\n    loopAfter: 900,\n    onReset: () => area.current?.leave(),\n    isEnabled: !isHovered,\n  });\n  const isDemoStopped = !isAutoplaying && !isHovered;\n  const leavesDemoCursor = isDemoStopped && !isStill;\n\n  /** Once a person took over, the cursor the demonstration drew goes: their own pointer draws it from now on. */\n  useEffect(() => {\n    if (leavesDemoCursor) {\n      area.current?.leave();\n    }\n  }, [leavesDemoCursor]);\n\n  useEffect(() => {\n    if (isStill) {\n      area.current?.glideTo(STILL_AT.across, STILL_AT.down);\n    }\n  }, [isStill]);\n\n  function playBeat(beat: DemoBeat) {\n    if (beat.kind === 'mode') {\n      setMode(beat.mode);\n      return;\n    }\n    if (beat.kind === 'glide') {\n      area.current?.glideTo(beat.place.across, beat.place.down);\n      return;\n    }\n    area.current?.leave();\n  }\n\n  function toggleTile(id: string) {\n    setSelectedId((current) => toggledSelection(current, id));\n  }\n\n  return (\n    <div\n      ref={ref}\n      className='flex w-full max-w-[22rem] flex-col items-center gap-3'\n    >\n      <div className='w-full' {...hoverHandlers}>\n        <CursorArea\n          handleRef={area}\n          mode={mode}\n          name='Ines Duarte'\n          color={seriesColorOf(2)}\n          className='bg-foreground/[0.04] rounded-xl'\n        >\n          <div className='grid grid-cols-2 gap-2 p-2.5'>\n            {TILES.map((tile) => (\n              <BoardTileButton\n                key={tile.id}\n                tile={tile}\n                isSelected={selectedId === tile.id}\n                onToggle={() => toggleTile(tile.id)}\n              />\n            ))}\n            <Input\n              aria-label='Comment on the board'\n              placeholder='Leave a comment'\n              className='col-span-2'\n            />\n          </div>\n        </CursorArea>\n      </div>\n      <Segments\n        value={mode}\n        segments={MODES}\n        onValueChange={setMode}\n        label='Cursor'\n      />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/effect/effect-03.tsx"
    },
    {
      "path": "registry/ndeko/components/effect/_parts/follower.ts",
      "content": "/** A frame that comes late, after a long task, moves the values no further than this would. */\nconst MAX_ELAPSED_S = 0.05;\n\n/** The spring is worked out in steps this short, so a stiff spring stays steady on a slow frame. */\nconst STEP_S = 1 / 120;\n\nexport type FollowerOptions = {\n  /** How hard each value is pulled to its target: about 120 glides, 700 trails a pointer closely. */\n  stiffness: number;\n  /** How close to its target a value counts as arrived, in its own unit. */\n  precision: number;\n};\n\nexport type Follower = {\n  /** Sends the values towards new targets. Frames run until every value has arrived, then stop. */\n  follow: (targets: readonly number[]) => void;\n  /** Puts the values on new targets at once and draws them, with no movement. */\n  place: (targets: readonly number[]) => void;\n  setStiffness: (stiffness: number) => void;\n  /** Asks for no more frames. The values stay where they are. */\n  stop: () => void;\n};\n\n/**\n * Values that follow their targets like a spring that does not bounce. Each frame hands them to\n * `draw`, which writes them to the page; no frame is asked for once they have all arrived.\n */\nexport function createFollower(\n  count: number,\n  draw: (values: readonly number[]) => void,\n  options: FollowerOptions\n): Follower {\n  const values = new Array<number>(count).fill(0);\n  const speeds = new Array<number>(count).fill(0);\n  const targets = new Array<number>(count).fill(0);\n  const { precision } = options;\n  let stiffness = options.stiffness;\n  let frame = 0;\n  let lastAt = 0;\n\n  function hasArrived() {\n    const settledSpeed = precision * Math.sqrt(stiffness) * 2;\n    for (let index = 0; index < count; index += 1) {\n      const gap = Math.abs(targets[index] - values[index]);\n      const speed = Math.abs(speeds[index]);\n      const isMoving = gap > precision || speed > settledSpeed;\n      if (isMoving) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  function land() {\n    for (let index = 0; index < count; index += 1) {\n      values[index] = targets[index];\n      speeds[index] = 0;\n    }\n  }\n\n  function advance(seconds: number) {\n    const damping = 2 * Math.sqrt(stiffness);\n    for (let index = 0; index < count; index += 1) {\n      const gap = targets[index] - values[index];\n      const force = stiffness * gap - damping * speeds[index];\n      speeds[index] += force * seconds;\n      values[index] += speeds[index] * seconds;\n    }\n  }\n\n  function tick(now: number) {\n    frame = 0;\n    const elapsedMs = now - lastAt;\n    lastAt = now;\n    const forward = Math.max(0, elapsedMs / 1000);\n    let left = Math.min(forward, MAX_ELAPSED_S);\n    while (left > 0) {\n      const step = Math.min(left, STEP_S);\n      advance(step);\n      left -= step;\n    }\n    if (hasArrived()) {\n      land();\n      draw(values);\n      return;\n    }\n    draw(values);\n    frame = requestAnimationFrame(tick);\n  }\n\n  function wake() {\n    if (frame !== 0) {\n      return;\n    }\n    lastAt = performance.now();\n    frame = requestAnimationFrame(tick);\n  }\n\n  function follow(next: readonly number[]) {\n    for (let index = 0; index < count; index += 1) {\n      targets[index] = next[index] ?? 0;\n    }\n    wake();\n  }\n\n  function stop() {\n    cancelAnimationFrame(frame);\n    frame = 0;\n  }\n\n  function place(next: readonly number[]) {\n    stop();\n    for (let index = 0; index < count; index += 1) {\n      targets[index] = next[index] ?? 0;\n    }\n    land();\n    draw(values);\n  }\n\n  function setStiffness(next: number) {\n    stiffness = next;\n  }\n\n  return { follow, place, setStiffness, stop };\n}\n",
      "type": "registry:component",
      "target": "components/effect/_parts/follower.ts"
    },
    {
      "path": "registry/ndeko/components/effect/_parts/rounded.ts",
      "content": "/**\n * Two engines can disagree on the last digit of a sine, and a frame writes many positions: a value\n * written into a style is rounded to the hundredth, so the server and the browser print the same\n * text and the style stays short.\n */\nexport function hundredthsOf(value: number) {\n  const hundredths = Math.round(value * 100);\n  return hundredths / 100;\n}\n",
      "type": "registry:component",
      "target": "components/effect/_parts/rounded.ts"
    },
    {
      "path": "registry/ndeko/components/effect/_parts/use-hover.ts",
      "content": "'use client';\n\nimport { useState } from 'react';\n\n/** Whether a mouse or a pen rests on an element. A finger does not hover, so a touch never counts. */\nexport function useHover() {\n  const [isHovered, setIsHovered] = useState(false);\n\n  function hoverStarts(event: React.PointerEvent<HTMLElement>) {\n    if (event.pointerType !== 'touch') {\n      setIsHovered(true);\n    }\n  }\n\n  function hoverEnds() {\n    setIsHovered(false);\n  }\n\n  const hoverHandlers = {\n    onPointerEnter: hoverStarts,\n    onPointerLeave: hoverEnds,\n  };\n\n  return { isHovered, hoverHandlers };\n}\n",
      "type": "registry:component",
      "target": "components/effect/_parts/use-hover.ts"
    }
  ],
  "categories": [
    "components",
    "effect"
  ],
  "type": "registry:component"
}