{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"layer-stack","type":"registry:ui","title":"Layer Stack","description":"Translucent design layers stacked in isometric view that fan apart on hover, with a layers panel to spotlight each one.","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"registry/ui/layer-stack.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useState, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface Layer {\n  label: string;\n  /** What is drawn on this layer. Fills the layer box. */\n  content: ReactNode;\n  /** Small icon shown in the layers panel. */\n  icon?: ReactNode;\n}\n\nexport interface LayerStackProps {\n  /** First item is the top-most layer. */\n  layers: Layer[];\n  /** Layer width in px. */\n  width?: number;\n  /** Layer height in px. */\n  height?: number;\n  /** Px between layers when fanned out. */\n  spread?: number;\n  /** Px between layers at rest. */\n  restGap?: number;\n  /** Camera pitch in degrees (0 = flat, 90 = edge-on). */\n  pitch?: number;\n  /** Keep the stack fanned out without hover. */\n  expanded?: boolean;\n  /** Show a Figma-style layers panel next to the stack. */\n  showPanel?: boolean;\n  className?: string;\n}\n\nconst EASE = \"cubic-bezier(.2,.8,.2,1)\";\n\nexport function LayerStack({\n  layers,\n  width = 260,\n  height = 180,\n  spread = 56,\n  restGap = 6,\n  pitch = 56,\n  expanded = false,\n  showPanel = true,\n  className,\n}: LayerStackProps) {\n  const [hover, setHover] = useState(false);\n  const [active, setActive] = useState<number | null>(null);\n  const open = expanded || hover || active !== null;\n  const n = layers.length;\n  const gap = open ? spread : restGap;\n  const rad = (pitch * Math.PI) / 180;\n  // Reserve the fanned-out footprint up front, so opening never spills out of the box:\n  // the rotated plane, the full vertical run of the spread stack (plus the active layer's lift),\n  // and room on the right for the labels.\n  const LABEL_ROOM = 160;\n  const planeW = (width + height) * Math.SQRT1_2 + 40;\n  const stageW = planeW + LABEL_ROOM;\n  const stageH = (width + height) * Math.SQRT1_2 * Math.cos(rad) + ((n - 1) * spread + 18) * Math.sin(rad) + 90;\n  const plane = `rotateX(${pitch}deg) rotateZ(-45deg)`;\n  const unplane = `rotateZ(45deg) rotateX(${-pitch}deg)`;\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-8\", className)}\n      onPointerLeave={() => {\n        setHover(false);\n        setActive(null);\n      }}\n    >\n      {showPanel && (\n        <div role=\"list\" aria-label=\"Layers\" className=\"w-44 shrink-0 rounded-xl border border-border bg-card/80 p-1.5 shadow-xl backdrop-blur\">\n          <p className=\"px-2.5 pt-1.5 pb-2 text-[11px] font-medium tracking-wider text-muted-foreground uppercase\">Layers</p>\n          {layers.map((layer, i) => (\n            <div role=\"listitem\" key={layer.label}>\n              <button\n                type=\"button\"\n                onPointerEnter={() => setActive(i)}\n                onFocus={() => setActive(i)}\n                onBlur={() => setActive(null)}\n                className={cn(\n                  \"flex w-full items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-left text-[13px] outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring\",\n                  active === i ? \"bg-muted text-foreground\" : \"text-muted-foreground hover:text-foreground\",\n                )}\n              >\n                <span aria-hidden className=\"grid size-4 place-items-center [&>svg]:size-full\">\n                  {layer.icon ?? <span className=\"size-2.5 rounded-sm border border-current\" />}\n                </span>\n                {layer.label}\n              </button>\n            </div>\n          ))}\n        </div>\n      )}\n\n      <div\n        aria-hidden\n        className=\"relative shrink-0\"\n        style={{ width: stageW, height: stageH }}\n        onPointerEnter={() => setHover(true)}\n      >\n        <div className=\"absolute top-1/2\" style={{ left: planeW / 2, transformStyle: \"preserve-3d\", transform: plane }}>\n          {layers.map((layer, i) => {\n            const z = (n - 1 - i - (n - 1) / 2) * gap + (active === i ? 18 : 0);\n            const dim = active !== null && active !== i;\n            // Stagger outward on open, inward on close.\n            const delay = (open ? i : n - 1 - i) * 35;\n            return (\n              <div\n                key={layer.label}\n                onPointerEnter={() => setActive(i)}\n                className=\"absolute motion-reduce:!transition-none\"\n                style={{\n                  width,\n                  height,\n                  left: -width / 2,\n                  top: -height / 2,\n                  transformStyle: \"preserve-3d\",\n                  transform: `translate3d(0,0,${z}px)`,\n                  opacity: dim ? 0.35 : 1,\n                  transition: `transform .7s ${EASE} ${delay}ms, opacity .35s`,\n                }}\n              >\n                <div\n                  className={cn(\n                    \"absolute inset-0 overflow-hidden rounded-2xl border transition-[border-color,box-shadow] duration-300\",\n                    active === i ? \"border-lime-300/80\" : \"border-white/15\",\n                  )}\n                  style={{\n                    background: \"color-mix(in oklab, var(--card) 22%, transparent)\",\n                    boxShadow: open ? \"0 30px 40px -20px rgba(0,0,0,.55)\" : \"none\",\n                  }}\n                >\n                  {layer.content}\n                </div>\n                {/* Label pinned to the right-hand corner, rotated back to face the viewer. */}\n                <div className=\"absolute top-full left-full\" style={{ transformStyle: \"preserve-3d\" }}>\n                  <div\n                    className=\"flex origin-top-left items-center gap-2 whitespace-nowrap motion-reduce:!transition-none\"\n                    style={{\n                      transform: `${unplane} translate(10px,-50%)`,\n                      opacity: open ? 1 : 0,\n                      transition: `opacity .4s ${open ? delay + 150 : 0}ms`,\n                    }}\n                  >\n                    <span className={cn(\"h-px w-6\", active === i ? \"bg-lime-300\" : \"bg-foreground/30\")} />\n                    <span\n                      className={cn(\n                        \"rounded-md border px-2 py-0.5 text-[11px] font-medium\",\n                        active === i ? \"border-lime-300/60 bg-lime-300 text-black\" : \"border-border bg-card/90 text-muted-foreground\",\n                      )}\n                    >\n                      {String(i + 1).padStart(2, \"0\")} {layer.label}\n                    </span>\n                  </div>\n                </div>\n              </div>\n            );\n          })}\n        </div>\n      </div>\n    </div>\n  );\n}\n"}]}