{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "projected-shadow",
  "title": "Projected Shadow",
  "description": "A CSS-powered primitive that layers an element with projected and contact shadows, supports an animation opt-out, and gathers on hover or active state.",
  "files": [
    {
      "path": "registry/base/ui/projected-shadow.tsx",
      "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport \"./projected-shadow.css\";\n\ntype ProjectedShadowTimingValue = number | string;\ntype ProjectedShadowLengthValue = number | string;\ntype CssVariableStyle = React.CSSProperties & Record<`--${string}`, string>;\n\nexport type ProjectedShadowOffset = {\n  /** Horizontal offset. Numeric values are converted to px. */\n  x?: ProjectedShadowLengthValue;\n  /** Vertical offset. Numeric values are converted to px. */\n  y?: ProjectedShadowLengthValue;\n};\n\n/**\n * Note: `children` is rendered up to three times — once in each decorative\n * shadow layer (projected and contact) and once as the visible target. The\n * shadow copies are `aria-hidden` and `inert`, so interactive children are not\n * duplicated as tab stops, but anything carrying an `id`, or a heavy node like\n * an `img`/`video`, is still duplicated in the DOM. Pass a lightweight `shadow`\n * to decouple the layers, or set `animated={false}` (or hide both layers) to\n * render `children` only once.\n */\nexport type ProjectedShadowProps = React.ComponentProps<\"span\"> & {\n  /** Keep the gathered state active without requiring hover. */\n  active?: boolean;\n  /** Enable the layered hover/focus animation. Set false for a neutral target. */\n  animated?: boolean;\n  /** Animation duration for release. Numeric values are converted to ms. */\n  duration?: ProjectedShadowTimingValue;\n  /** Animation duration for gather. Numeric values are converted to ms. */\n  activeDuration?: ProjectedShadowTimingValue;\n  /** CSS easing used by the transform and opacity transitions. */\n  ease?: string;\n  /** Blur radius for the far projected shadow. Numeric values are converted to px. */\n  projectedShadowBlur?: ProjectedShadowLengthValue;\n  /** Resting offset for the far projected shadow. */\n  projectedShadowOffset?: ProjectedShadowOffset;\n  /**\n   * Keep the wrapped content selectable on touch. Off by default, because the\n   * press that gathers the layers would otherwise raise iOS's selection menu\n   * partway through. Turn it on when wrapping copy rather than a mark, and\n   * accept that a long press interrupts the gather.\n   */\n  selectable?: boolean;\n  /** Optional decorative content for the shadow layers. Defaults to children. */\n  shadow?: React.ReactNode;\n  /** Classes applied to the visible animated child wrapper. */\n  targetClassName?: string;\n  /** Classes applied to the far projected shadow layer. */\n  projectedShadowClassName?: string;\n  /** Classes applied to the close contact shadow layer. */\n  contactShadowClassName?: string;\n  /** Render the far projected shadow layer. Defaults to true. */\n  showProjectedShadow?: boolean;\n  /** Render the close contact shadow layer. Defaults to true. */\n  showContactShadow?: boolean;\n};\n\nexport function ProjectedShadow({\n  active,\n  activeDuration,\n  animated = true,\n  children,\n  className,\n  contactShadowClassName,\n  duration,\n  ease,\n  projectedShadowClassName,\n  projectedShadowBlur,\n  projectedShadowOffset,\n  selectable,\n  shadow,\n  showContactShadow = true,\n  showProjectedShadow = true,\n  style,\n  targetClassName,\n  ...props\n}: ProjectedShadowProps) {\n  const shadowContent = shadow ?? children;\n\n  return (\n    <span\n      data-slot=\"projected-shadow\"\n      data-active={animated && active ? \"\" : undefined}\n      data-animated={animated ? \"\" : undefined}\n      data-selectable={selectable ? \"\" : undefined}\n      className={cn(\n        \"projected-shadow relative inline-flex items-center justify-center\",\n        className,\n      )}\n      style={getProjectedShadowStyle(\n        {\n          activeDuration,\n          duration,\n          ease,\n          projectedShadowBlur,\n          projectedShadowOffset,\n        },\n        style,\n      )}\n      {...props}\n    >\n      {animated && showProjectedShadow ? (\n        <span\n          aria-hidden=\"true\"\n          inert\n          data-slot=\"projected-shadow-projected\"\n          className={cn(\n            \"projected-shadow-layer projected-shadow-projected pointer-events-none absolute inset-0 z-0 inline-flex text-current\",\n            projectedShadowClassName,\n          )}\n        >\n          {shadowContent}\n        </span>\n      ) : null}\n\n      {animated && showContactShadow ? (\n        <span\n          aria-hidden=\"true\"\n          inert\n          data-slot=\"projected-shadow-contact\"\n          className={cn(\n            \"projected-shadow-layer projected-shadow-contact pointer-events-none absolute inset-0 z-0 inline-flex text-current\",\n            contactShadowClassName,\n          )}\n        >\n          {shadowContent}\n        </span>\n      ) : null}\n\n      <span\n        data-slot=\"projected-shadow-target\"\n        className={cn(\n          \"projected-shadow-layer projected-shadow-target relative z-10 inline-flex text-current\",\n          targetClassName,\n        )}\n      >\n        {children}\n      </span>\n    </span>\n  );\n}\n\nfunction getProjectedShadowStyle(\n  {\n    activeDuration,\n    duration,\n    ease,\n    projectedShadowBlur,\n    projectedShadowOffset,\n  }: Pick<\n    ProjectedShadowProps,\n    | \"activeDuration\"\n    | \"duration\"\n    | \"ease\"\n    | \"projectedShadowBlur\"\n    | \"projectedShadowOffset\"\n  >,\n  style: React.CSSProperties | undefined,\n) {\n  if (\n    activeDuration === undefined &&\n    duration === undefined &&\n    ease === undefined &&\n    projectedShadowBlur === undefined &&\n    projectedShadowOffset?.x === undefined &&\n    projectedShadowOffset?.y === undefined\n  ) {\n    return style;\n  }\n\n  return {\n    ...style,\n    ...(activeDuration !== undefined\n      ? { \"--projected-shadow-active-duration\": toCssTime(activeDuration) }\n      : {}),\n    ...(duration !== undefined\n      ? { \"--projected-shadow-duration\": toCssTime(duration) }\n      : {}),\n    ...(ease !== undefined ? { \"--projected-shadow-ease\": ease } : {}),\n    ...(projectedShadowBlur !== undefined\n      ? { \"--projected-shadow-blur\": toCssLength(projectedShadowBlur) }\n      : {}),\n    ...(projectedShadowOffset?.x !== undefined\n      ? {\n          \"--projected-shadow-projected-x\": toCssLength(\n            projectedShadowOffset.x,\n          ),\n        }\n      : {}),\n    ...(projectedShadowOffset?.y !== undefined\n      ? {\n          \"--projected-shadow-projected-y\": toCssLength(\n            projectedShadowOffset.y,\n          ),\n        }\n      : {}),\n  } as CssVariableStyle;\n}\n\nfunction toCssTime(value: ProjectedShadowTimingValue) {\n  return typeof value === \"number\" ? `${value}ms` : value;\n}\n\nfunction toCssLength(value: ProjectedShadowLengthValue) {\n  return typeof value === \"number\" ? `${value}px` : value;\n}\n",
      "type": "registry:ui",
      "target": "@ui/projected-shadow.tsx"
    },
    {
      "path": "registry/base/ui/projected-shadow.css",
      "content": "/* Defaults are layered for customization. Normal unlayered styles and\n   utilities in a later layer can override them. */\n@layer components {\n  .projected-shadow {\n    --projected-shadow-duration: 280ms;\n    --projected-shadow-active-duration: 360ms;\n    --projected-shadow-ease: cubic-bezier(0.165, 0.84, 0.44, 1);\n    --projected-shadow-origin: center;\n    --projected-shadow-blur: 6px;\n    --projected-shadow-projected-opacity: 1;\n    --projected-shadow-projected-active-opacity: 0;\n    --projected-shadow-projected-x: -14%;\n    --projected-shadow-projected-y: -18%;\n    --projected-shadow-projected-scale: 1.08;\n    --projected-shadow-projected-rotate: -8deg;\n    --projected-shadow-projected-active-x: -4%;\n    --projected-shadow-projected-active-y: -5%;\n    --projected-shadow-projected-active-scale: 0.94;\n    --projected-shadow-projected-active-rotate: -3deg;\n    --projected-shadow-contact-opacity: 0;\n    --projected-shadow-contact-active-opacity: 0.72;\n    --projected-shadow-contact-x: -4%;\n    --projected-shadow-contact-y: -5%;\n    --projected-shadow-contact-scale: 0.92;\n    --projected-shadow-contact-rotate: -2deg;\n    --projected-shadow-contact-active-x: 0%;\n    --projected-shadow-contact-active-y: 0%;\n    --projected-shadow-contact-active-scale: 0.89;\n    --projected-shadow-contact-active-rotate: 0deg;\n    --projected-shadow-target-x: 8%;\n    --projected-shadow-target-y: -6%;\n    --projected-shadow-target-scale: 1.06;\n    --projected-shadow-target-rotate: 10deg;\n    --projected-shadow-target-active-x: 0%;\n    --projected-shadow-target-active-y: 0%;\n    --projected-shadow-target-active-scale: 0.86;\n    --projected-shadow-target-active-rotate: 0deg;\n  }\n\n  .projected-shadow-layer {\n    backface-visibility: hidden;\n    transform-origin: var(--projected-shadow-origin);\n  }\n\n  .projected-shadow[data-animated] .projected-shadow-layer {\n    transition-property: opacity, transform;\n    transition-duration: var(--projected-shadow-duration);\n    transition-timing-function: var(--projected-shadow-ease);\n  }\n\n  /* Promote only while the gather can actually run. Three layers per instance,\n     permanently promoted, is real GPU memory in a grid of these. `:active` is\n     free to list here even though only touch gathers on it: a mouse press\n     implies hover and a keyboard press implies focus, so on those devices the\n     layer is already promoted by the time this matches. */\n  .projected-shadow[data-animated]:hover .projected-shadow-layer,\n  .projected-shadow[data-animated]:focus-within .projected-shadow-layer,\n  .projected-shadow[data-animated]:active .projected-shadow-layer,\n  .projected-shadow[data-animated][data-active] .projected-shadow-layer {\n    will-change: transform, opacity;\n  }\n\n  .projected-shadow[data-animated] .projected-shadow-projected {\n    filter: blur(var(--projected-shadow-blur));\n    opacity: var(--projected-shadow-projected-opacity);\n    transform: translate3d(\n        var(--projected-shadow-projected-x),\n        var(--projected-shadow-projected-y),\n        0\n      )\n      scale(var(--projected-shadow-projected-scale))\n      rotate(var(--projected-shadow-projected-rotate));\n  }\n\n  .projected-shadow[data-animated] .projected-shadow-contact {\n    opacity: var(--projected-shadow-contact-opacity);\n    transform: translate3d(\n        var(--projected-shadow-contact-x),\n        var(--projected-shadow-contact-y),\n        0\n      )\n      scale(var(--projected-shadow-contact-scale))\n      rotate(var(--projected-shadow-contact-rotate));\n  }\n\n  .projected-shadow[data-animated] .projected-shadow-target {\n    transform: translate3d(\n        var(--projected-shadow-target-x),\n        var(--projected-shadow-target-y),\n        0\n      )\n      scale(var(--projected-shadow-target-scale))\n      rotate(var(--projected-shadow-target-rotate));\n  }\n\n  .projected-shadow[data-animated][data-active] .projected-shadow-layer {\n    transition-duration: var(--projected-shadow-active-duration);\n  }\n\n  .projected-shadow[data-animated][data-active] .projected-shadow-projected {\n    opacity: var(--projected-shadow-projected-active-opacity);\n    transform: translate3d(\n        var(--projected-shadow-projected-active-x),\n        var(--projected-shadow-projected-active-y),\n        0\n      )\n      scale(var(--projected-shadow-projected-active-scale))\n      rotate(var(--projected-shadow-projected-active-rotate));\n  }\n\n  .projected-shadow[data-animated][data-active] .projected-shadow-contact {\n    opacity: var(--projected-shadow-contact-active-opacity);\n    transform: translate3d(\n        var(--projected-shadow-contact-active-x),\n        var(--projected-shadow-contact-active-y),\n        0\n      )\n      scale(var(--projected-shadow-contact-active-scale))\n      rotate(var(--projected-shadow-contact-active-rotate));\n  }\n\n  .projected-shadow[data-animated][data-active] .projected-shadow-target {\n    transform: translate3d(\n        var(--projected-shadow-target-active-x),\n        var(--projected-shadow-target-active-y),\n        0\n      )\n      scale(var(--projected-shadow-target-active-scale))\n      rotate(var(--projected-shadow-target-active-rotate));\n  }\n\n  @media (hover: hover) and (pointer: fine) {\n    .projected-shadow[data-animated]:hover .projected-shadow-layer {\n      transition-duration: var(--projected-shadow-active-duration);\n    }\n\n    .projected-shadow[data-animated]:hover .projected-shadow-projected {\n      opacity: var(--projected-shadow-projected-active-opacity);\n      transform: translate3d(\n          var(--projected-shadow-projected-active-x),\n          var(--projected-shadow-projected-active-y),\n          0\n        )\n        scale(var(--projected-shadow-projected-active-scale))\n        rotate(var(--projected-shadow-projected-active-rotate));\n    }\n\n    .projected-shadow[data-animated]:hover .projected-shadow-contact {\n      opacity: var(--projected-shadow-contact-active-opacity);\n      transform: translate3d(\n          var(--projected-shadow-contact-active-x),\n          var(--projected-shadow-contact-active-y),\n          0\n        )\n        scale(var(--projected-shadow-contact-active-scale))\n        rotate(var(--projected-shadow-contact-active-rotate));\n    }\n\n    .projected-shadow[data-animated]:hover .projected-shadow-target {\n      transform: translate3d(\n          var(--projected-shadow-target-active-x),\n          var(--projected-shadow-target-active-y),\n          0\n        )\n        scale(var(--projected-shadow-target-active-scale))\n        rotate(var(--projected-shadow-target-active-rotate));\n    }\n  }\n\n  /* Touch parity: a press stands in for the hover the device cannot deliver,\n     so the gather is reachable without the consumer wiring up `active`.\n     `cursor: pointer` is the signal Safari uses to decide an element is\n     clickable and therefore eligible for `:active`; where there is no hover\n     there is no cursor, so declaring it costs nothing visually. */\n  @media (hover: none) {\n    .projected-shadow[data-animated] {\n      cursor: pointer;\n      -webkit-tap-highlight-color: transparent;\n    }\n\n    /* The callout and selection suppressions keep a press-and-hold from turning\n       into iOS's text selection menu halfway through the gather. They inherit\n       into whatever is wrapped, so a consumer wrapping real copy rather than a\n       mark opts back out with `selectable` and accepts the interruption. */\n    .projected-shadow[data-animated]:not([data-selectable]) {\n      -webkit-touch-callout: none;\n      user-select: none;\n    }\n\n    .projected-shadow[data-animated]:active .projected-shadow-layer {\n      transition-duration: var(--projected-shadow-active-duration);\n    }\n\n    .projected-shadow[data-animated]:active .projected-shadow-projected {\n      opacity: var(--projected-shadow-projected-active-opacity);\n      transform: translate3d(\n          var(--projected-shadow-projected-active-x),\n          var(--projected-shadow-projected-active-y),\n          0\n        )\n        scale(var(--projected-shadow-projected-active-scale))\n        rotate(var(--projected-shadow-projected-active-rotate));\n    }\n\n    .projected-shadow[data-animated]:active .projected-shadow-contact {\n      opacity: var(--projected-shadow-contact-active-opacity);\n      transform: translate3d(\n          var(--projected-shadow-contact-active-x),\n          var(--projected-shadow-contact-active-y),\n          0\n        )\n        scale(var(--projected-shadow-contact-active-scale))\n        rotate(var(--projected-shadow-contact-active-rotate));\n    }\n\n    .projected-shadow[data-animated]:active .projected-shadow-target {\n      transform: translate3d(\n          var(--projected-shadow-target-active-x),\n          var(--projected-shadow-target-active-y),\n          0\n        )\n        scale(var(--projected-shadow-target-active-scale))\n        rotate(var(--projected-shadow-target-active-rotate));\n    }\n  }\n\n  /* Keyboard parity with the hover gather when the wrapped child is focusable. */\n  .projected-shadow[data-animated]:focus-within .projected-shadow-layer {\n    transition-duration: var(--projected-shadow-active-duration);\n  }\n\n  .projected-shadow[data-animated]:focus-within .projected-shadow-projected {\n    opacity: var(--projected-shadow-projected-active-opacity);\n    transform: translate3d(\n        var(--projected-shadow-projected-active-x),\n        var(--projected-shadow-projected-active-y),\n        0\n      )\n      scale(var(--projected-shadow-projected-active-scale))\n      rotate(var(--projected-shadow-projected-active-rotate));\n  }\n\n  .projected-shadow[data-animated]:focus-within .projected-shadow-contact {\n    opacity: var(--projected-shadow-contact-active-opacity);\n    transform: translate3d(\n        var(--projected-shadow-contact-active-x),\n        var(--projected-shadow-contact-active-y),\n        0\n      )\n      scale(var(--projected-shadow-contact-active-scale))\n      rotate(var(--projected-shadow-contact-active-rotate));\n  }\n\n  .projected-shadow[data-animated]:focus-within .projected-shadow-target {\n    transform: translate3d(\n        var(--projected-shadow-target-active-x),\n        var(--projected-shadow-target-active-y),\n        0\n      )\n      scale(var(--projected-shadow-target-active-scale))\n      rotate(var(--projected-shadow-target-active-rotate));\n  }\n}\n\n/* Motion guards override ordinary customization, including inline timing.\n   Keep these outside the defaults layer; only motion declarations are important. */\n@media (prefers-reduced-motion: reduce) {\n  .projected-shadow[data-animated] {\n    --projected-shadow-blur: 0px;\n    --projected-shadow-projected-opacity: 0;\n    --projected-shadow-projected-active-opacity: 0;\n    --projected-shadow-projected-x: 0%;\n    --projected-shadow-projected-y: 0%;\n    --projected-shadow-projected-scale: 1;\n    --projected-shadow-projected-rotate: 0deg;\n    --projected-shadow-projected-active-x: 0%;\n    --projected-shadow-projected-active-y: 0%;\n    --projected-shadow-projected-active-scale: 1;\n    --projected-shadow-projected-active-rotate: 0deg;\n    --projected-shadow-contact-opacity: 0;\n    --projected-shadow-contact-active-opacity: 0;\n    --projected-shadow-contact-x: 0%;\n    --projected-shadow-contact-y: 0%;\n    --projected-shadow-contact-scale: 1;\n    --projected-shadow-contact-rotate: 0deg;\n    --projected-shadow-contact-active-x: 0%;\n    --projected-shadow-contact-active-y: 0%;\n    --projected-shadow-contact-active-scale: 1;\n    --projected-shadow-contact-active-rotate: 0deg;\n    --projected-shadow-target-x: 0%;\n    --projected-shadow-target-y: 0%;\n    --projected-shadow-target-scale: 1;\n    --projected-shadow-target-rotate: 0deg;\n    --projected-shadow-target-active-x: 0%;\n    --projected-shadow-target-active-y: 0%;\n    --projected-shadow-target-active-scale: 1;\n    --projected-shadow-target-active-rotate: 0deg;\n  }\n\n  .projected-shadow[data-animated] .projected-shadow-layer {\n    transition-duration: 0ms !important;\n    transition-delay: 0ms !important;\n    will-change: auto;\n  }\n}\n",
      "type": "registry:file",
      "target": "@ui/projected-shadow.css"
    }
  ],
  "meta": {
    "tags": [
      "hover",
      "shadow",
      "projection",
      "contact-shadow",
      "layered-shadow",
      "primitive"
    ],
    "effects": [
      "projected-shadow",
      "hover-gather",
      "scale",
      "rotate",
      "reduced-motion"
    ]
  },
  "categories": [
    "motion-primitive"
  ],
  "type": "registry:ui"
}