{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"An EmbossSurface component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"motion/react\";\nimport {\n  type CSSProperties,\n  type ElementType,\n  type ReactNode,\n  type PointerEvent as ReactPointerEvent,\n  useCallback,\n  useEffect,\n  useId,\n  useState,\n} from \"react\";\n\nexport type EmbossSurfaceVariant =\n  | \"emboss\"\n  | \"deboss\"\n  | \"plaster\"\n  | \"metal-stamp\";\n\nexport interface EmbossSurfaceProps {\n  /** Element or component rendered as the material surface. Defaults to `div`. */\n  as?: ElementType;\n  /** Content that receives the relief. */\n  children: ReactNode;\n  /** Extra classes for the surface. */\n  className?: string;\n  /** Base material colour. Any CSS colour; defaults to the `background` token. */\n  color?: string;\n  /** Relief depth in pixels. */\n  depth?: number;\n  /** Let the light angle follow the pointer on hover-capable devices. */\n  interactive?: boolean;\n  /** Direction the light comes from, in degrees. `0` is right, `90` is up. */\n  lightAngle?: number;\n  /** Shadow diffusion, `0` (hard) to `1` (soft). */\n  softness?: number;\n  /** Material recipe used to build the relief. */\n  variant?: EmbossSurfaceVariant;\n}\n\ninterface VariantSpec {\n  blurScale: number;\n  hardEdge: boolean;\n  highlightMix: number;\n  inset: boolean;\n  invert: boolean;\n  shadeMix: number;\n  specular: boolean;\n}\n\nconst HOVER_QUERY = \"(hover: hover) and (pointer: fine)\";\nconst DEFAULT_DEPTH = 2;\nconst DEFAULT_LIGHT_ANGLE = 135;\nconst DEFAULT_SOFTNESS = 0.5;\nconst DEFAULT_COLOR = \"var(--color-background)\";\nconst SOFTNESS_SCALE = 2.4;\nconst MIN_BLUR = 0.25;\nconst DEGREES_PER_TURN = 360;\nconst HARD_LAYER_SCALE = 1.8;\nconst DROP_SHADOW_BLUR_SCALE = 0.6;\nconst SPECULAR_ELEVATION = 42;\nconst SPECULAR_CONSTANT = 0.9;\nconst SPECULAR_EXPONENT = 18;\nconst SPECULAR_SURFACE_SCALE = 3;\nconst RADIANS_PER_DEGREE = Math.PI / 180;\nconst UNSAFE_ID_CHARS = /[^a-zA-Z0-9_-]/g;\n\nconst VARIANTS: Record<EmbossSurfaceVariant, VariantSpec> = {\n  deboss: {\n    blurScale: 1,\n    hardEdge: false,\n    highlightMix: 46,\n    inset: true,\n    invert: true,\n    shadeMix: 34,\n    specular: false,\n  },\n  emboss: {\n    blurScale: 1,\n    hardEdge: false,\n    highlightMix: 46,\n    inset: false,\n    invert: false,\n    shadeMix: 34,\n    specular: false,\n  },\n  \"metal-stamp\": {\n    blurScale: 0.45,\n    hardEdge: true,\n    highlightMix: 72,\n    inset: false,\n    invert: false,\n    shadeMix: 58,\n    specular: false,\n  },\n  plaster: {\n    blurScale: 1.8,\n    hardEdge: false,\n    highlightMix: 64,\n    inset: true,\n    invert: true,\n    shadeMix: 26,\n    specular: true,\n  },\n};\n\nconst BLUR_VAR = \"calc(var(--es-blur) * 1px)\";\n\nconst offsetLayer = (scale: number, blur: string, tint: string) =>\n  `calc(var(--es-dx) * ${scale}px) calc(var(--es-dy) * ${scale}px) ${blur} ${tint}`;\n\nconst buildShadow = (\n  spec: VariantSpec,\n  highlight: string,\n  shade: string,\n  prefix: string\n) => {\n  const sign = spec.invert ? -1 : 1;\n  const layers = [\n    `${prefix}${offsetLayer(sign, BLUR_VAR, highlight)}`,\n    `${prefix}${offsetLayer(-sign, BLUR_VAR, shade)}`,\n  ];\n  if (spec.hardEdge) {\n    layers.push(\n      `${prefix}${offsetLayer(sign * HARD_LAYER_SCALE, \"0px\", highlight)}`\n    );\n    layers.push(\n      `${prefix}${offsetLayer(-sign * HARD_LAYER_SCALE, \"0px\", shade)}`\n    );\n  }\n  return layers.join(\", \");\n};\n\nconst EmbossSurface = ({\n  as,\n  children,\n  className,\n  color = DEFAULT_COLOR,\n  depth = DEFAULT_DEPTH,\n  interactive = false,\n  lightAngle = DEFAULT_LIGHT_ANGLE,\n  softness = DEFAULT_SOFTNESS,\n  variant = \"emboss\",\n}: EmbossSurfaceProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [isHoverDevice, setIsHoverDevice] = useState(false);\n  const filterId = useId().replace(UNSAFE_ID_CHARS, \"\");\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || !window.matchMedia) {\n      return;\n    }\n    const query = window.matchMedia(HOVER_QUERY);\n    setIsHoverDevice(query.matches);\n    const handleChange = (event: MediaQueryListEvent) => {\n      setIsHoverDevice(event.matches);\n    };\n    query.addEventListener(\"change\", handleChange);\n    return () => {\n      query.removeEventListener(\"change\", handleChange);\n    };\n  }, []);\n\n  const spec = VARIANTS[variant];\n  const radians = lightAngle * RADIANS_PER_DEGREE;\n  const lightX = Math.cos(radians) * depth;\n  const lightY = -Math.sin(radians) * depth;\n  const blur = Math.max(\n    MIN_BLUR,\n    depth * softness * SOFTNESS_SCALE * spec.blurScale\n  );\n\n  const highlight = `color-mix(in oklab, ${color}, white ${spec.highlightMix}%)`;\n  const shade = `color-mix(in oklab, ${color}, black ${spec.shadeMix}%)`;\n\n  const tracksPointer = interactive && !shouldReduceMotion && isHoverDevice;\n\n  const handlePointerMove = useCallback(\n    (event: ReactPointerEvent<HTMLElement>) => {\n      const element = event.currentTarget;\n      const rect = element.getBoundingClientRect();\n      const deltaX = event.clientX - (rect.left + rect.width / 2);\n      const deltaY = event.clientY - (rect.top + rect.height / 2);\n      const length = Math.hypot(deltaX, deltaY);\n      if (length === 0) {\n        return;\n      }\n      element.style.setProperty(\"--es-dx\", `${(deltaX / length) * depth}`);\n      element.style.setProperty(\"--es-dy\", `${(deltaY / length) * depth}`);\n    },\n    [depth]\n  );\n\n  const handlePointerLeave = useCallback(\n    (event: ReactPointerEvent<HTMLElement>) => {\n      const element = event.currentTarget;\n      element.style.setProperty(\"--es-dx\", `${lightX}`);\n      element.style.setProperty(\"--es-dy\", `${lightY}`);\n    },\n    [lightX, lightY]\n  );\n\n  const dropShadow = spec.hardEdge\n    ? `drop-shadow(${offsetLayer(1, `calc(var(--es-blur) * ${DROP_SHADOW_BLUR_SCALE}px)`, highlight)}) drop-shadow(${offsetLayer(-1, `calc(var(--es-blur) * ${DROP_SHADOW_BLUR_SCALE}px)`, shade)})`\n    : undefined;\n\n  const style = {\n    \"--es-blur\": blur,\n    \"--es-dx\": lightX,\n    \"--es-dy\": lightY,\n    backgroundColor: color,\n    boxShadow: buildShadow(spec, highlight, shade, spec.inset ? \"inset \" : \"\"),\n    filter: spec.specular ? `url(#${filterId})` : dropShadow,\n    textShadow: buildShadow(spec, highlight, shade, \"\"),\n    transition: shouldReduceMotion\n      ? undefined\n      : \"box-shadow 240ms cubic-bezier(0.645, 0.045, 0.355, 1)\",\n  } as CSSProperties;\n\n  const Tag: ElementType = as ?? \"div\";\n\n  return (\n    <>\n      {spec.specular ? (\n        <svg\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute h-0 w-0\"\n        >\n          <title>Plaster relief filter</title>\n          <filter id={filterId}>\n            <feGaussianBlur\n              in=\"SourceAlpha\"\n              result=\"relief\"\n              stdDeviation={depth}\n            />\n            <feSpecularLighting\n              in=\"relief\"\n              lightingColor=\"white\"\n              result=\"specular\"\n              specularConstant={SPECULAR_CONSTANT}\n              specularExponent={SPECULAR_EXPONENT}\n              surfaceScale={SPECULAR_SURFACE_SCALE}\n            >\n              <feDistantLight\n                azimuth={(DEGREES_PER_TURN - lightAngle) % DEGREES_PER_TURN}\n                elevation={SPECULAR_ELEVATION}\n              />\n            </feSpecularLighting>\n            <feComposite\n              in=\"specular\"\n              in2=\"SourceAlpha\"\n              operator=\"in\"\n              result=\"clipped\"\n            />\n            <feComposite\n              in=\"SourceGraphic\"\n              in2=\"clipped\"\n              k1=\"0\"\n              k2=\"1\"\n              k3=\"1\"\n              k4=\"0\"\n              operator=\"arithmetic\"\n            />\n          </filter>\n        </svg>\n      ) : null}\n      <Tag\n        className={cn(\"text-foreground\", className)}\n        onPointerLeave={tracksPointer ? handlePointerLeave : undefined}\n        onPointerMove={tracksPointer ? handlePointerMove : undefined}\n        style={style}\n      >\n        {children}\n      </Tag>\n    </>\n  );\n};\n\nexport default EmbossSurface;\n","path":"index.tsx","target":"components/smoothui/emboss-surface/index.tsx","type":"registry:ui"}],"name":"emboss-surface","registryDependencies":[],"title":"Emboss Surface","type":"registry:ui"}