{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"press-depth","type":"registry:ui","title":"Press Depth","description":"The feeling that the press landed.","dependencies":["motion"],"categories":["action feedback"],"docs":"https://www.interior.dev/docs/press-depth","files":[{"path":"registry/interior/press-depth.tsx","type":"registry:ui","target":"components/interior/press-depth.tsx","content":"\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst PRESS = { type: \"spring\", stiffness: 520, damping: 34, mass: 0.45 } as const;\n\nexport type UsePressDepthOptions = {\n  disabled?: boolean;\n  onPressStart?: () => void;\n  onPressEnd?: () => void;\n};\n\nexport type PressOrigin = { x: number; y: number };\n\nexport type UsePressDepthResult = {\n  pressed: boolean;\n  origin: PressOrigin | null;\n  ref: (node: HTMLElement | null) => void;\n  bind: {\n    onPointerDown: (event: React.PointerEvent) => void;\n    onKeyDown: (event: React.KeyboardEvent) => void;\n    onKeyUp: (event: React.KeyboardEvent) => void;\n    onBlur: () => void;\n  };\n};\n\nexport function usePressDepth(\n  options: UsePressDepthOptions = {},\n): UsePressDepthResult {\n  const { disabled = false, onPressStart, onPressEnd } = options;\n\n  const [pressed, setPressed] = useState(false);\n  const [tracking, setTracking] = useState(false);\n  const [origin, setOrigin] = useState<PressOrigin | null>(null);\n\n  const node = useRef<HTMLElement | null>(null);\n  const pointer = useRef<number | null>(null);\n  const down = useRef(false);\n\n  const began = useRef(onPressStart);\n  began.current = onPressStart;\n  const ended = useRef(onPressEnd);\n  ended.current = onPressEnd;\n\n  const setDown = useCallback((next: boolean) => {\n    if (down.current === next) return;\n    down.current = next;\n    setPressed(next);\n    if (next) began.current?.();\n    else ended.current?.();\n  }, []);\n\n  const stop = useCallback(() => {\n    pointer.current = null;\n    setTracking(false);\n    setOrigin(null);\n    setDown(false);\n  }, [setDown]);\n\n  useEffect(() => {\n    if (!tracking) return;\n\n    const contains = (event: PointerEvent) => {\n      const el = node.current;\n      if (!el) return false;\n      const r = el.getBoundingClientRect();\n      return (\n        event.clientX >= r.left &&\n        event.clientX <= r.right &&\n        event.clientY >= r.top &&\n        event.clientY <= r.bottom\n      );\n    };\n\n    const move = (event: PointerEvent) => {\n      if (event.pointerId !== pointer.current) return;\n      setDown(contains(event));\n    };\n    const lift = (event: PointerEvent) => {\n      if (event.pointerId !== pointer.current) return;\n      stop();\n    };\n    const bail = () => stop();\n    const hidden = () => {\n      if (document.hidden) stop();\n    };\n\n    window.addEventListener(\"pointermove\", move);\n    window.addEventListener(\"pointerup\", lift);\n    window.addEventListener(\"pointercancel\", lift);\n    window.addEventListener(\"blur\", bail);\n    document.addEventListener(\"visibilitychange\", hidden);\n\n    return () => {\n      window.removeEventListener(\"pointermove\", move);\n      window.removeEventListener(\"pointerup\", lift);\n      window.removeEventListener(\"pointercancel\", lift);\n      window.removeEventListener(\"blur\", bail);\n      document.removeEventListener(\"visibilitychange\", hidden);\n    };\n  }, [tracking, setDown, stop]);\n\n  useEffect(() => {\n    if (disabled) stop();\n  }, [disabled, stop]);\n\n  const ref = useCallback((next: HTMLElement | null) => {\n    node.current = next;\n  }, []);\n\n  const bind = {\n    onPointerDown: (event: React.PointerEvent) => {\n      if (disabled) return;\n      if (event.pointerType === \"mouse\" && event.button !== 0) return;\n      const r = event.currentTarget.getBoundingClientRect();\n      setOrigin({\n        x: Math.max(-1, Math.min(1, ((event.clientX - r.left) / r.width) * 2 - 1)),\n        y: Math.max(-1, Math.min(1, ((event.clientY - r.top) / r.height) * 2 - 1)),\n      });\n      pointer.current = event.pointerId;\n      setTracking(true);\n      setDown(true);\n    },\n    onKeyDown: (event: React.KeyboardEvent) => {\n      if (disabled || event.repeat) return;\n      if (event.key === \" \" || event.key === \"Enter\") setDown(true);\n    },\n    onKeyUp: (event: React.KeyboardEvent) => {\n      if (event.key === \" \" || event.key === \"Enter\" || event.key === \"Escape\") {\n        setDown(false);\n      }\n    },\n    onBlur: () => stop(),\n  };\n\n  return { pressed, origin, ref, bind };\n}\n\nexport type PressDepthProps = {\n  children: React.ReactNode;\n  depth?: number;\n  tilt?: number;\n  disabled?: boolean;\n  type?: \"button\" | \"submit\" | \"reset\";\n  onClick?: React.MouseEventHandler<HTMLButtonElement>;\n  className?: string;\n  \"aria-label\"?: string;\n};\n\nexport function PressDepth({\n  children,\n  depth = 4,\n  tilt = 7,\n  disabled = false,\n  type = \"button\",\n  onClick,\n  className = \"\",\n  \"aria-label\": ariaLabel,\n}: PressDepthProps) {\n  const reduced = useReducedMotion();\n  const { pressed, origin, ref, bind } = usePressDepth({ disabled });\n\n  const lean = pressed && origin && !reduced ? origin : null;\n\n  return (\n    <button\n      ref={ref}\n      type={type}\n      disabled={disabled}\n      aria-label={ariaLabel}\n      data-pressed={pressed ? \"\" : undefined}\n      onClick={onClick}\n      style={{\n        paddingBottom: depth,\n        touchAction: \"manipulation\",\n        WebkitTapHighlightColor: \"transparent\",\n      }}\n      className=\"group relative inline-flex select-none rounded-[9px] align-middle outline-none disabled:opacity-50\"\n      {...bind}\n    >\n      <span\n        aria-hidden\n        style={{ top: depth }}\n        className=\"absolute inset-x-0 bottom-0 rounded-[9px] bg-stone-300 dark:bg-white/25\"\n      />\n      <motion.span\n        initial={false}\n        animate={{\n          y: pressed ? depth : 0,\n          rotateX: lean ? -lean.y * tilt : 0,\n          rotateY: lean ? lean.x * tilt : 0,\n        }}\n        transition={reduced ? { duration: 0 } : PRESS}\n        style={{ transformPerspective: 340 }}\n        className={`relative inline-flex h-9 items-center justify-center gap-2 rounded-[9px] border border-stone-200 bg-white px-3.5 text-[13px] font-medium text-stone-700 group-focus-visible:ring-2 group-focus-visible:ring-stone-400 dark:border-white/[0.16] dark:bg-[#1D1D1A] dark:text-stone-200 dark:group-focus-visible:ring-stone-500 ${className}`}\n      >\n        <motion.span\n          aria-hidden\n          initial={false}\n          animate={{ opacity: pressed ? 0 : 1 }}\n          transition={reduced ? { duration: 0 } : PRESS}\n          className=\"pointer-events-none absolute inset-0 rounded-[9px] shadow-[inset_0_1.5px_0_rgba(255,255,255,0.95),inset_0_-1px_0_rgba(28,25,23,0.06)] dark:shadow-[inset_0_1.5px_0_rgba(255,255,255,0.09)]\"\n        />\n        {children}\n      </motion.span>\n    </button>\n  );\n}\n"}]}