{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sticker-peel",
  "type": "registry:ui",
  "title": "Sticker Peel",
  "description": "A card whose corner lifts off the page on hover, folded back over the crease to show its backing.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/loomui/sticker-peel.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface StickerPeelProps extends React.ComponentProps<\"div\"> {\n  /** How far the corner lifts, as a CSS length. */\n  size?: string\n  /** Which corner peels. */\n  corner?: \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\"\n  /** Length of the peel in milliseconds. */\n  duration?: number\n  /** Hold the corner up. Leave it out to peel on hover and focus. */\n  peeled?: boolean\n}\n\n/**\n * Each corner is one cut and one flap. The flap is the cut triangle mirrored\n * back over the crease, which is why its tip lands inside the sticker rather\n * than on top of the corner it came from.\n */\nconst CORNERS = {\n  \"top-left\": {\n    face: (size: string) =>\n      `polygon(${size} 0, 100% 0, 100% 100%, 0 100%, 0 ${size})`,\n    flap: \"polygon(0 100%, 100% 0, 100% 100%)\",\n    anchor: \"top-0 left-0 origin-top-left\",\n    sheen: \"bg-linear-to-br\",\n  },\n  \"top-right\": {\n    face: (size: string) =>\n      `polygon(0 0, calc(100% - ${size}) 0, 100% ${size}, 100% 100%, 0 100%)`,\n    flap: \"polygon(0 0, 100% 100%, 0 100%)\",\n    anchor: \"top-0 right-0 origin-top-right\",\n    sheen: \"bg-linear-to-bl\",\n  },\n  \"bottom-left\": {\n    face: (size: string) =>\n      `polygon(0 0, 100% 0, 100% 100%, ${size} 100%, 0 calc(100% - ${size}))`,\n    flap: \"polygon(0 0, 100% 100%, 100% 0)\",\n    anchor: \"bottom-0 left-0 origin-bottom-left\",\n    sheen: \"bg-linear-to-tr\",\n  },\n  \"bottom-right\": {\n    face: (size: string) =>\n      `polygon(0 0, 100% 0, 100% calc(100% - ${size}), calc(100% - ${size}) 100%, 0 100%)`,\n    flap: \"polygon(100% 0, 0 100%, 0 0)\",\n    anchor: \"bottom-0 right-0 origin-bottom-right\",\n    sheen: \"bg-linear-to-tl\",\n  },\n} as const\n\nexport function StickerPeel({\n  children,\n  size = \"2.75rem\",\n  corner = \"bottom-right\",\n  duration = 380,\n  peeled,\n  className,\n  onPointerEnter,\n  onPointerLeave,\n  onFocus,\n  onBlur,\n  ...props\n}: StickerPeelProps) {\n  const [hovered, setHovered] = React.useState(false)\n  const isControlled = peeled !== undefined\n  const lifted = isControlled ? peeled : hovered\n  const spec = CORNERS[corner]\n\n  return (\n    <div\n      data-slot=\"sticker-peel\"\n      data-peeled={lifted ? \"\" : undefined}\n      className={cn(\"relative isolate\", className)}\n      onPointerEnter={(event) => {\n        onPointerEnter?.(event)\n        setHovered(true)\n      }}\n      onPointerLeave={(event) => {\n        onPointerLeave?.(event)\n        setHovered(false)\n      }}\n      // Focus bubbles, so anything focusable inside the sticker peels it too\n      // and the corner is not a hover-only affordance.\n      onFocus={(event) => {\n        onFocus?.(event)\n        setHovered(true)\n      }}\n      onBlur={(event) => {\n        onBlur?.(event)\n        setHovered(false)\n      }}\n      {...props}\n    >\n      {/* Both polygons have the same five points, so the browser interpolates\n          the cut instead of snapping between two shapes. */}\n      <div\n        className=\"h-full w-full transition-[clip-path] ease-[var(--ease-out-quart)] motion-reduce:transition-none\"\n        style={{\n          clipPath: spec.face(lifted ? size : \"0px\"),\n          transitionDuration: `${duration}ms`,\n        }}\n      >\n        {children}\n      </div>\n\n      <span\n        data-slot=\"sticker-peel-flap\"\n        aria-hidden=\"true\"\n        className={cn(\n          \"pointer-events-none absolute transition-transform ease-[var(--ease-out-quart)] motion-reduce:transition-none\",\n          // The underside is backing paper, not the surface it came off, so it\n          // keeps its own neutral rather than following the theme's card. It\n          // stays lighter than the sticker in both modes, which is the only\n          // thing that reads as \"lifted\" instead of \"cut out\".\n          \"from-zinc-300 via-zinc-100 to-white\",\n          \"dark:from-zinc-800 dark:via-zinc-600 dark:to-zinc-400\",\n          // Dark mode has no ambient contrast to fall back on, so the crease\n          // shadow carries the separation and has to be deeper there.\n          \"drop-shadow-[0_2px_6px_rgb(0_0_0/0.28)] dark:drop-shadow-[0_3px_10px_rgb(0_0_0/0.55)]\",\n          spec.anchor,\n          spec.sheen\n        )}\n        style={{\n          width: size,\n          height: size,\n          clipPath: spec.flap,\n          // Grown from the corner rather than sized, so the flap and the cut\n          // move on the same curve and the crease never splits.\n          transform: lifted ? \"scale(1)\" : \"scale(0)\",\n          transitionDuration: `${duration}ms`,\n        }}\n      />\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "cssVars": {
    "theme": {
      "ease-out-quart": "cubic-bezier(0.165, 0.84, 0.44, 1)"
    }
  }
}