{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-lens",
  "title": "Glass Lens",
  "description": "A draggable magnifier whose glass rim bends the content beneath it.",
  "dependencies": [
    "clsx"
  ],
  "registryDependencies": [
    "@glassyui/glass-surface"
  ],
  "files": [
    {
      "path": "registry/glassyui/glass-lens.tsx",
      "content": "'use client';\n\nimport clsx from 'clsx';\nimport { forwardRef, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport type { CSSProperties, HTMLAttributes, ReactNode } from 'react';\nimport { mergeRefs } from './lib/merge-refs';\nimport { useMagnifierFilter } from './lib/use-refraction-filter';\nimport { useOptionalPreferences } from './glass-provider';\n\ntype LensPoint = { x: number; y: number };\n\ntype LensStageSize = { height: number; width: number };\n\ntype LensStyle = CSSProperties & Record<`--lg-lens-${string}`, string>;\n\nexport type GlassLensProps = HTMLAttributes<HTMLDivElement> & {\n  /**\n   * The content to magnify. It renders a second time inside the lens, so give it its own\n   * background and avoid ids.\n   */\n  children: ReactNode;\n  /** Starting centre of the lens, as fractions of the stage. */\n  defaultPosition?: LensPoint;\n  /** Accessible name of the draggable lens. */\n  label?: string;\n  /** Diameter in pixels. */\n  size?: number;\n  zoom?: number;\n};\n\nconst ARROW_DIRECTIONS = new Map<string, LensPoint>([\n  ['ArrowUp', { x: 0, y: -1 }],\n  ['ArrowDown', { x: 0, y: 1 }],\n  ['ArrowLeft', { x: -1, y: 0 }],\n  ['ArrowRight', { x: 1, y: 0 }],\n]);\n\nconst KEY_STEP = 10;\n\nconst KEY_STEP_LARGE = 50;\n\nconst EDGE_MARGIN = 4;\n\nfunction clampToStage(value: number, extent: number) {\n  return Math.min(Math.max(value, EDGE_MARGIN), Math.max(EDGE_MARGIN, extent - EDGE_MARGIN));\n}\n\n/**\n * A draggable magnifier over any content. The zoomed copy bends into the rim through a\n * generated SVG filter, which works in every engine that supports SVG filters on HTML;\n * reduced motion and reduced transparency keep a plain magnifier.\n */\nexport const GlassLens = forwardRef<HTMLDivElement, GlassLensProps>(\n  (\n    {\n      children,\n      className,\n      defaultPosition,\n      label = 'Magnifier',\n      size = 160,\n      zoom = 1.6,\n      ...props\n    },\n    forwardedRef\n  ) => {\n    const { preferences } = useOptionalPreferences();\n    const stageRef = useRef<HTMLDivElement | null>(null);\n    const lensRef = useRef<HTMLDivElement | null>(null);\n    const ref = useMemo(() => mergeRefs(stageRef, forwardedRef), [forwardedRef]);\n    const [stage, setStage] = useState<LensStageSize | null>(null);\n\n    const centreRef = useRef<LensPoint>({\n      x: defaultPosition?.x ?? 0.5,\n      y: defaultPosition?.y ?? 0.5,\n    });\n\n    const grabRef = useRef<LensPoint | null>(null);\n\n    const { filterElement, filterStyle, margin } = useMagnifierFilter({\n      enabled: stage !== null && !preferences.reduceMotion && !preferences.reduceTransparency,\n      size,\n    });\n\n    // The centre is kept as stage fractions so the lens stays over the same spot on resize.\n    // Only a move rewrites them; placing reads the live stage, so a resize that lands\n    // between observer callbacks cannot shift the lens.\n    const place = () => {\n      const stageElement = stageRef.current;\n      const lens = lensRef.current;\n\n      if (!stageElement || !lens || !stageElement.clientWidth || !stageElement.clientHeight) {\n        return;\n      }\n\n      const width = stageElement.clientWidth;\n      const height = stageElement.clientHeight;\n\n      lens.style.setProperty('--lg-lens-x', `${clampToStage(centreRef.current.x * width, width)}px`);\n      lens.style.setProperty('--lg-lens-y', `${clampToStage(centreRef.current.y * height, height)}px`);\n    };\n\n    const moveTo = (x: number, y: number) => {\n      const stageElement = stageRef.current;\n\n      if (!stageElement || !stageElement.clientWidth || !stageElement.clientHeight) return;\n\n      const width = stageElement.clientWidth;\n      const height = stageElement.clientHeight;\n\n      centreRef.current = { x: clampToStage(x, width) / width, y: clampToStage(y, height) / height };\n      place();\n    };\n\n    useEffect(() => {\n      const stageElement = stageRef.current;\n\n      if (!stageElement) return;\n\n      const observer = new ResizeObserver(() => {\n        setStage({ height: stageElement.clientHeight, width: stageElement.clientWidth });\n      });\n\n      observer.observe(stageElement);\n\n      return () => observer.disconnect();\n    }, []);\n\n    useLayoutEffect(() => {\n      if (stage) place();\n    });\n\n    const stagePoint = (event: React.PointerEvent<HTMLDivElement>) => {\n      const rect = stageRef.current?.getBoundingClientRect();\n\n      return { x: event.clientX - (rect?.left ?? 0), y: event.clientY - (rect?.top ?? 0) };\n    };\n\n    const currentCentre = () => ({\n      x: centreRef.current.x * (stageRef.current?.clientWidth ?? 0),\n      y: centreRef.current.y * (stageRef.current?.clientHeight ?? 0),\n    });\n\n    const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {\n      if (event.button !== 0) return;\n\n      const point = stagePoint(event);\n      const centre = currentCentre();\n\n      event.currentTarget.setPointerCapture(event.pointerId);\n      grabRef.current = { x: point.x - centre.x, y: point.y - centre.y };\n    };\n\n    const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {\n      const grab = grabRef.current;\n\n      if (!grab) return;\n\n      const point = stagePoint(event);\n\n      moveTo(point.x - grab.x, point.y - grab.y);\n    };\n\n    const handlePointerEnd = () => {\n      grabRef.current = null;\n    };\n\n    const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n      const direction = ARROW_DIRECTIONS.get(event.key);\n\n      if (!direction) return;\n\n      const step = event.shiftKey ? KEY_STEP_LARGE : KEY_STEP;\n      const centre = currentCentre();\n\n      event.preventDefault();\n      moveTo(centre.x + direction.x * step, centre.y + direction.y * step);\n    };\n\n    const lensStyle: LensStyle = {\n      '--lg-lens-margin': `${margin}px`,\n      '--lg-lens-size': `${size}px`,\n      '--lg-lens-stage-height': `${stage?.height ?? 0}px`,\n      '--lg-lens-stage-width': `${stage?.width ?? 0}px`,\n      '--lg-lens-zoom': `${zoom}`,\n    };\n\n    return (\n      <div ref={ref} className={clsx('lg-lens-stage', className)} {...props}>\n        {children}\n        {stage ? (\n          <div\n            ref={lensRef}\n            aria-keyshortcuts=\"ArrowUp ArrowDown ArrowLeft ArrowRight\"\n            aria-label={label}\n            aria-roledescription=\"magnifier\"\n            className=\"lg-lens\"\n            role=\"group\"\n            style={lensStyle}\n            tabIndex={0}\n            onKeyDown={handleKeyDown}\n            onLostPointerCapture={handlePointerEnd}\n            onPointerCancel={handlePointerEnd}\n            onPointerDown={handlePointerDown}\n            onPointerMove={handlePointerMove}\n            onPointerUp={handlePointerEnd}\n          >\n            {filterElement}\n            <div aria-hidden className=\"lg-lens-view\" inert style={{ filter: filterStyle }}>\n              <div className=\"lg-lens-scene\">{children}</div>\n            </div>\n          </div>\n        ) : null}\n      </div>\n    );\n  }\n);\n\nGlassLens.displayName = 'GlassLens';\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-lens.tsx"
    }
  ],
  "meta": {
    "sourceModules": [
      "lens"
    ]
  },
  "type": "registry:ui"
}