{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "envelope",
  "title": "Envelope",
  "description": "Animated envelope UI: a composer where the letter folds into the envelope, gets sealed and flies off, plus a presentational envelope that opens to reveal any content.",
  "dependencies": [
    "gsap",
    "@gsap/react",
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://trytweenly.vercel.app/r/use-reduced-motion.json",
    "https://trytweenly.vercel.app/r/tweenly-theme.json"
  ],
  "files": [
    {
      "path": "registry/new-york/envelope/envelope.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useEffectEvent, useId, useRef, useState } from \"react\"\nimport { AnimatePresence, motion, useAnimate, useInView, type Transition } from \"motion/react\"\nimport gsap from \"gsap\"\nimport { useGSAP } from \"@gsap/react\"\nimport { Check, PenLine, Plane, Send } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { useReducedMotion } from \"@/registry/new-york/hooks/use-reduced-motion\"\n\ngsap.registerPlugin(useGSAP)\n\n/* ------------------------------------------------------------------ types */\n\nexport type EnvelopeVariant = \"classic\" | \"airmail\" | \"minimal\" | \"glass\"\nexport type EnvelopeFlyDirection = \"up-right\" | \"up\" | \"right\"\nexport type EnvelopeTrigger = \"click\" | \"hover\" | \"inView\" | \"manual\"\nexport type EnvelopeField = \"name\" | \"email\" | \"subject\" | \"message\"\n\nexport interface EnvelopeValues {\n  name: string\n  email: string\n  subject: string\n  message: string\n}\n\nexport interface EnvelopeFields {\n  /** Show the sender name field (required when shown). Default: true */\n  name?: boolean\n  /** Show the email field (validated when filled). Default: true */\n  email?: boolean\n  /** Show the subject field. Default: false */\n  subject?: boolean\n}\n\ninterface EnvelopeLook {\n  /** Look of the envelope, letter and seal. Default: \"classic\" */\n  variant?: EnvelopeVariant\n  /** Envelope color (any CSS color). Default: per variant (kraft, white, `var(--card)`, frosted white) */\n  color?: string\n  /** Letter paper color (any CSS color). Default: per variant */\n  paper?: string\n  /** Letter text color (any CSS color). Default: per variant */\n  ink?: string\n  /** Wax seal / stamp / dot color (any CSS color). Default: per variant (red wax, red stamp, accent) */\n  seal?: string\n  /** Accent used for buttons, focus, glow and the minimal seal. Default: \"#ff4d12\" */\n  accent?: string\n  /** Initial or monogram on the seal or stamp. Default: \"T\" (composer: the sender's initial) */\n  sealLabel?: string\n  /** Animation speed multiplier; 2 plays the choreography twice as slow. Default: 1 */\n  duration?: number\n  /** Envelope width in px (shrinks to fit its container). Default: 420 */\n  width?: number\n  /** Envelope corner radius in px. Default: 14 */\n  radius?: number\n  /** Additional classes for the root element. */\n  className?: string\n}\n\nexport interface EnvelopeComposerProps extends EnvelopeLook {\n  /** Which fields to show. The message field is always shown. Default: { name: true, email: true, subject: false } */\n  fields?: EnvelopeFields\n  /** Placeholder per field. Default: { name: \"Your name\", email: \"you@example.com\", subject: \"What's it about?\", message: \"Write your message…\" } */\n  placeholders?: Partial<Record<EnvelopeField, string>>\n  /** Visible label per field. Default: { name: \"From\", email: \"Email\", subject: \"Subject\", message: \"Message\" } */\n  labels?: Partial<Record<EnvelopeField, string>>\n  /** Maximum message length, shown as a live counter. Default: 500 */\n  messageMaxLength?: number\n  /** Send button text. Default: \"Send\" */\n  sendLabel?: string\n  /** Button text in the success state. Default: \"Write another\" */\n  againLabel?: string\n  /** Success heading, or a function of the sent values. Default: (v) => \"Sent. Thanks, {first name}.\" */\n  successTitle?: string | ((values: EnvelopeValues) => string)\n  /** Success body, or a function of the sent values. Default: \"Your letter is on its way. We'll write back soon.\" */\n  successMessage?: string | ((values: EnvelopeValues) => string)\n  /** Shown when `onSend` rejects (an Error's message is used when present). Default: \"Couldn't send. Try again.\" */\n  errorMessage?: string\n  /** Called with the trimmed values. Return a promise to hold the envelope until it settles; a rejection reopens it. */\n  onSend?: (values: EnvelopeValues) => Promise<void> | void\n  /** Where the sealed envelope flies off to. Default: \"up-right\" */\n  flyDirection?: EnvelopeFlyDirection\n  /** Bring a fresh envelope back automatically after this many ms, or false to wait for \"Write another\". Default: false */\n  resetAfter?: number | false\n}\n\nexport interface EnvelopeProps extends EnvelopeLook {\n  /** Content of the letter, e.g. an invitation or a thank-you note. */\n  children?: React.ReactNode\n  /** Controlled open state. */\n  open?: boolean\n  /** Initial open state when uncontrolled. Default: false */\n  defaultOpen?: boolean\n  /** Called when the envelope asks to open or close. */\n  onOpenChange?: (open: boolean) => void\n  /** What opens the envelope. \"manual\" only responds to `open`. Default: \"click\" */\n  trigger?: EnvelopeTrigger\n  /** Accessible label for the open/close button. Default: \"Open envelope\" */\n  label?: string\n  /** Classes for the letter paper. */\n  letterClassName?: string\n}\n\n/* ---------------------------------------------------------------- palette */\n\nconst ACCENT = \"#ff4d12\"\nconst ERROR = \"#e5484d\"\nconst EASE_OUT = [0.22, 1, 0.36, 1] as const\nconst spring: Transition = { type: \"spring\", stiffness: 460, damping: 32 }\n\n/** Flap depth as a fraction of the envelope height; the seal sits on its tip. */\nconst FLAP = 56\nconst FOLD = 0.34\nconst FLAP_CLIP = \"polygon(0 0, 100% 0, 50% 100%)\"\nconst FLAP_INNER_CLIP = \"polygon(5% 0, 95% 0, 50% 89%)\"\nconst FRONT_CLIP = `polygon(0 0, 50% ${FLAP}%, 100% 0, 100% 100%, 0 100%)`\n// Hard-edged airmail stripes (a pattern, no color blending)\nconst STRIPES =\n  \"repeating-linear-gradient(-45deg, #d62839 0 9px, transparent 9px 15px, #1f4aa8 15px 24px, transparent 24px 30px)\"\n\n/** Two flat fold lines across the letter: a dark hairline with a light edge below. */\nfunction CreaseLines() {\n  return [33.2, 66.5].map((top) => (\n    <span\n      key={top}\n      className=\"absolute inset-x-0 h-px\"\n      style={{ top: `${top}%`, backgroundColor: \"rgba(0,0,0,.1)\", boxShadow: \"0 1px 0 rgba(255,255,255,.45)\" }}\n    />\n  ))\n}\n\nconst mix = (a: string, pct: number, b: string) => `color-mix(in oklab, ${a} ${pct}%, ${b})`\n\ntype SealKind = \"wax\" | \"stamp\" | \"dot\" | \"glow\"\n\ninterface Palette {\n  env: string\n  back: string\n  flap: string\n  flapInner: string\n  crease: string\n  frontInk: string\n  paper: string\n  ink: string\n  muted: string\n  seal: string\n  accent: string\n  font: string\n  kind: SealKind\n  glass: boolean\n  stripes: boolean\n  outline: boolean\n}\n\ntype LookInput = Pick<EnvelopeLook, \"color\" | \"paper\" | \"ink\" | \"seal\"> & { accent: string }\n\nfunction palette(variant: EnvelopeVariant, o: LookInput): Palette {\n  const accent = o.accent || ACCENT\n  if (variant === \"airmail\") {\n    const env = o.color || \"#fbfaf6\"\n    const ink = o.ink || \"#1f2a44\"\n    return {\n      env,\n      back: mix(env, 84, \"#1f2a44\"),\n      flap: env,\n      flapInner: mix(env, 78, \"#1f2a44\"),\n      crease: \"rgba(31,42,68,0.16)\",\n      frontInk: mix(ink, 60, \"transparent\"),\n      paper: o.paper || \"#ffffff\",\n      ink,\n      muted: mix(ink, 55, \"transparent\"),\n      seal: o.seal || \"#c8102e\",\n      accent,\n      font: \"font-mono\",\n      kind: \"stamp\",\n      glass: false,\n      stripes: true,\n      outline: false,\n    }\n  }\n  if (variant === \"minimal\") {\n    const env = o.color || \"var(--card)\"\n    const ink = o.ink || \"var(--foreground)\"\n    return {\n      env,\n      back: mix(env, 93, \"var(--foreground)\"),\n      flap: env,\n      flapInner: mix(env, 90, \"var(--foreground)\"),\n      crease: mix(\"var(--foreground)\", 14, \"transparent\"),\n      frontInk: \"var(--muted-foreground)\",\n      paper: o.paper || \"var(--panel)\",\n      ink,\n      muted: mix(ink, 50, \"transparent\"),\n      seal: o.seal || accent,\n      accent,\n      font: \"font-sans\",\n      kind: \"dot\",\n      glass: false,\n      stripes: false,\n      outline: true,\n    }\n  }\n  if (variant === \"glass\") {\n    const env = o.color || \"rgba(255,255,255,0.16)\"\n    const ink = o.ink || \"#0a0a0a\"\n    return {\n      env,\n      back: mix(env, 50, \"transparent\"),\n      flap: mix(env, 80, \"rgba(255,255,255,0.3)\"),\n      flapInner: \"rgba(255,255,255,0.08)\",\n      crease: \"rgba(255,255,255,0.45)\",\n      frontInk: \"rgba(255,255,255,0.9)\",\n      paper: o.paper || \"rgba(255,255,255,0.92)\",\n      ink,\n      muted: mix(ink, 50, \"transparent\"),\n      seal: o.seal || accent,\n      accent,\n      font: \"font-sans\",\n      kind: \"glow\",\n      glass: true,\n      stripes: false,\n      outline: true,\n    }\n  }\n  const env = o.color || \"#d9b68a\"\n  const ink = o.ink || \"#2b2118\"\n  return {\n    env,\n    back: mix(env, 72, \"#3a220c\"),\n    flap: env,\n    flapInner: mix(env, 70, \"#3a220c\"),\n    crease: mix(env, 62, \"#3a220c\"),\n    frontInk: mix(env, 40, \"#2a1808\"),\n    paper: o.paper || \"#fbf6ea\",\n    ink,\n    muted: mix(ink, 55, \"transparent\"),\n    seal: o.seal || \"#a51d1d\",\n    accent,\n    font: \"font-serif\",\n    kind: \"wax\",\n    glass: false,\n    stripes: false,\n    outline: false,\n  }\n}\n\n/* ------------------------------------------------------------------- seal */\n\nconst WAX_PATH = (() => {\n  const pts: string[] = []\n  const n = 60\n  for (let i = 0; i < n; i++) {\n    const a = (i / n) * Math.PI * 2\n    const r = 45 + 2.2 * Math.sin(a * 7 + 0.6) + 1.3 * Math.sin(a * 13 + 2.1) + 0.8 * Math.cos(a * 5)\n    pts.push(`${(50 + r * Math.cos(a)).toFixed(2)} ${(50 + r * Math.sin(a)).toFixed(2)}`)\n  }\n  return `M${pts.join(\" L\")} Z`\n})()\n\nconst SEAL_WIDTH: Record<SealKind, string> = { wax: \"17%\", stamp: \"16%\", dot: \"10%\", glow: \"13%\" }\n\n// Alpha mask that punches the perforated stamp edge, not a color gradient\nconst PERFORATION =\n  \"radial-gradient(circle at 50% 50%, transparent 2.2px, #000 2.7px) -4px -4px / 8px 8px, linear-gradient(#000 0 0) content-box\"\n\nfunction SealMark({ p, label }: { p: Palette; label: string }) {\n  const text = label.slice(0, 3)\n\n  if (p.kind === \"wax\") {\n    const size = text.length > 1 ? 24 : 36\n    const common = {\n      x: 50,\n      y: 51,\n      textAnchor: \"middle\" as const,\n      dominantBaseline: \"central\" as const,\n      fontFamily: \"Georgia, 'Times New Roman', serif\",\n      fontWeight: 700,\n      fontSize: size,\n    }\n    return (\n      <svg viewBox=\"0 0 100 100\" className=\"block h-full w-full overflow-visible drop-shadow-[0_4px_5px_rgba(0,0,0,0.35)]\" aria-hidden=\"true\">\n        <path d={WAX_PATH} fill={p.seal} />\n        <circle cx=\"50\" cy=\"50\" r=\"31\" fill=\"none\" stroke=\"rgba(0,0,0,0.3)\" strokeWidth=\"2.6\" />\n        <circle cx=\"50.8\" cy=\"50.8\" r=\"31\" fill=\"none\" stroke=\"rgba(255,255,255,0.16)\" strokeWidth=\"1\" />\n        <text {...common} fill=\"rgba(255,255,255,0.28)\" transform=\"translate(-0.9 -0.9)\">{text}</text>\n        <text {...common} fill=\"rgba(0,0,0,0.42)\" transform=\"translate(1 1)\">{text}</text>\n        <text {...common} fill={p.seal}>{text}</text>\n      </svg>\n    )\n  }\n\n  if (p.kind === \"stamp\") {\n    return (\n      <div className=\"relative h-full w-full\">\n        <div className=\"h-full w-full drop-shadow-[0_2px_3px_rgba(0,0,0,0.22)]\">\n          <div className=\"h-full w-full bg-white p-[5px]\" style={{ mask: PERFORATION, WebkitMask: PERFORATION }}>\n            <div\n              className=\"flex h-full w-full flex-col items-center justify-center gap-[6%] rounded-[1px] text-white\"\n              style={{ background: p.seal }}\n            >\n              <Plane className=\"h-auto w-[42%] -rotate-12\" strokeWidth={1.75} aria-hidden=\"true\" />\n              <span className=\"font-mono text-[9px] font-bold leading-none tracking-wider\">{text}</span>\n            </div>\n          </div>\n        </div>\n        <svg\n          data-postmark\n          viewBox=\"0 0 140 100\"\n          className=\"pointer-events-none absolute -left-[62%] -top-[6%] w-[190%] overflow-visible\"\n          fill=\"none\"\n          stroke=\"rgba(31,42,68,0.55)\"\n          aria-hidden=\"true\"\n        >\n          <circle cx=\"44\" cy=\"50\" r=\"33\" strokeWidth=\"2.4\" />\n          <circle cx=\"44\" cy=\"50\" r=\"25\" strokeWidth=\"1.2\" />\n          <text x=\"44\" y=\"46\" textAnchor=\"middle\" fontSize=\"8\" fontFamily=\"ui-monospace, monospace\" fontWeight=\"700\" fill=\"rgba(31,42,68,0.6)\" stroke=\"none\">PAR AVION</text>\n          <text x=\"44\" y=\"58\" textAnchor=\"middle\" fontSize=\"7\" fontFamily=\"ui-monospace, monospace\" fill=\"rgba(31,42,68,0.6)\" stroke=\"none\">AIR MAIL</text>\n          {[36, 50, 64].map((y) => (\n            <path key={y} d={`M80 ${y} q7 -6 14 0 t14 0 t14 0 t14 0`} strokeWidth=\"2.4\" strokeLinecap=\"round\" />\n          ))}\n        </svg>\n      </div>\n    )\n  }\n\n  if (p.kind === \"dot\") {\n    return (\n      <div\n        className=\"grid aspect-square h-full w-full place-items-center rounded-full text-white\"\n        style={{ background: p.seal, boxShadow: `0 0 0 3px ${p.env}, 0 6px 14px -4px ${mix(p.seal, 60, \"transparent\")}` }}\n      >\n        <Check className=\"h-auto w-[52%]\" strokeWidth={3} aria-hidden=\"true\" />\n      </div>\n    )\n  }\n\n  return (\n    <div\n      className=\"grid aspect-square h-full w-full place-items-center rounded-full font-semibold text-white\"\n      style={{\n        background: p.seal,\n        boxShadow: `inset 0 0 0 1px rgba(255,255,255,.45), 0 0 26px 6px ${mix(p.seal, 55, \"transparent\")}`,\n      }}\n    >\n      <span className=\"text-[15px] leading-none\">{text}</span>\n    </div>\n  )\n}\n\n/* --------------------------------------------------------------- envelope */\n\ninterface BodyProps {\n  p: Palette\n  radius: number\n  sealLabel: string\n  flapOpen: boolean\n  sealShown: boolean\n  flapRef: React.RefObject<HTMLDivElement | null>\n  flapInnerRef: React.RefObject<HTMLDivElement | null>\n  sealRef: React.RefObject<HTMLDivElement | null>\n  front?: React.ReactNode\n  children?: React.ReactNode\n}\n\n/** Back, flap, front pocket and seal. Layers: back, open flap (10), letter (20), front (25), closed flap (30), seal (40). */\nfunction EnvelopeBody({ p, radius, sealLabel, flapOpen, sealShown, flapRef, flapInnerRef, sealRef, front, children }: BodyProps) {\n  const glassFx = p.glass ? \"blur(14px) saturate(1.4)\" : undefined\n  const line = p.outline ? `1px solid ${p.crease}` : undefined\n\n  return (\n    <div className=\"relative aspect-[16/9] w-full\">\n      {/* back / inside */}\n      <div\n        className=\"absolute inset-0\"\n        style={{\n          borderRadius: radius,\n          backgroundColor: p.back,\n          boxShadow: \"0 28px 50px -24px rgba(0,0,0,.45), 0 2px 6px rgba(0,0,0,.08)\",\n          border: line,\n          backdropFilter: glassFx,\n          WebkitBackdropFilter: glassFx,\n        }}\n      />\n\n      {children}\n\n      {/* flap */}\n      <div\n        ref={flapRef}\n        className=\"pointer-events-none absolute inset-x-0 top-0\"\n        style={{\n          height: `${FLAP}%`,\n          zIndex: flapOpen ? 10 : 30,\n          transformOrigin: \"50% 0%\",\n          transform: flapOpen ? \"perspective(900px) rotateX(180deg)\" : undefined,\n          filter: p.glass ? undefined : \"drop-shadow(0 3px 3px rgba(0,0,0,.16))\",\n        }}\n      >\n        <div\n          className=\"absolute inset-0\"\n          style={{\n            clipPath: FLAP_CLIP,\n            borderTopLeftRadius: radius,\n            borderTopRightRadius: radius,\n            backgroundColor: p.flap,\n            backgroundImage: p.stripes ? STRIPES : undefined,\n            backdropFilter: glassFx,\n            WebkitBackdropFilter: glassFx,\n          }}\n        >\n          {p.stripes && <div className=\"absolute inset-0\" style={{ clipPath: FLAP_INNER_CLIP, background: p.flap }} />}\n          <div ref={flapInnerRef} className=\"absolute inset-0\" style={{ background: p.flapInner, opacity: flapOpen ? 1 : 0 }} />\n          <svg className=\"absolute inset-0 h-full w-full\" viewBox=\"0 0 100 100\" preserveAspectRatio=\"none\" aria-hidden=\"true\">\n            <polyline points=\"0,0 50,100 100,0\" fill=\"none\" stroke={p.crease} strokeWidth=\"2\" vectorEffect=\"non-scaling-stroke\" />\n          </svg>\n        </div>\n      </div>\n\n      {/* front pocket */}\n      <div\n        className=\"absolute inset-0 z-[25]\"\n        style={{\n          clipPath: FRONT_CLIP,\n          borderRadius: radius,\n          backgroundColor: p.env,\n          backdropFilter: glassFx,\n          WebkitBackdropFilter: glassFx,\n          ...(p.stripes\n            ? { borderStyle: \"solid\", borderWidth: \"0 6px 6px 6px\", borderImage: `${STRIPES} 6` }\n            : { border: line }),\n        }}\n      >\n        <svg className=\"pointer-events-none absolute inset-0 h-full w-full\" viewBox=\"0 0 100 100\" preserveAspectRatio=\"none\" aria-hidden=\"true\">\n          <polyline points={`0,0 50,${FLAP} 100,0`} fill=\"none\" stroke={p.crease} strokeWidth=\"2\" vectorEffect=\"non-scaling-stroke\" />\n          <polyline points=\"0,100 50,64 100,100\" fill=\"none\" stroke={p.crease} strokeWidth=\"1\" vectorEffect=\"non-scaling-stroke\" />\n        </svg>\n        {front}\n      </div>\n\n      {/* seal */}\n      <div\n        className=\"pointer-events-none absolute left-1/2 z-40 -translate-x-1/2 -translate-y-1/2\"\n        style={{ top: `${FLAP}%`, width: SEAL_WIDTH[p.kind] }}\n      >\n        <div ref={sealRef} className={p.kind === \"stamp\" ? \"aspect-[4/5]\" : \"aspect-square\"} style={{ opacity: sealShown ? 1 : 0 }}>\n          <SealMark p={p} label={sealLabel} />\n        </div>\n      </div>\n    </div>\n  )\n}\n\nfunction GlassGlow({ accent }: { accent: string }) {\n  return (\n    <div\n      aria-hidden=\"true\"\n      className=\"pointer-events-none absolute -inset-[10%] -z-10\"\n    >\n      {/* Flat tinted shapes; the glass backdrop blur softens them where they sit behind the envelope. */}\n      <span className=\"absolute left-[16%] top-[34%] aspect-square w-[34%] rounded-full\" style={{ backgroundColor: mix(accent, 60, \"transparent\") }} />\n      <span className=\"absolute right-[14%] top-[12%] aspect-square w-[30%] rounded-full\" style={{ backgroundColor: \"rgba(109,93,252,.45)\" }} />\n    </div>\n  )\n}\n\nconst vars = (p: Palette, style: React.CSSProperties) =>\n  ({ ...style, \"--env-accent\": p.accent, \"--env-crease\": p.crease, \"--env-error\": ERROR }) as React.CSSProperties\n\nconst shadowOf = (p: Palette) =>\n  p.outline\n    ? `0 0 0 1px ${p.crease}, 0 10px 30px -14px rgba(0,0,0,.35)`\n    : \"0 1px 2px rgba(0,0,0,.08), 0 12px 28px -12px rgba(0,0,0,.35)\"\n\n/* --------------------------------------------------------------- composer */\n\nconst EMPTY: EnvelopeValues = { name: \"\", email: \"\", subject: \"\", message: \"\" }\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nconst DEFAULT_PLACEHOLDERS: Record<EnvelopeField, string> = {\n  name: \"Your name\",\n  email: \"you@example.com\",\n  subject: \"What's it about?\",\n  message: \"Write your message…\",\n}\nconst DEFAULT_LABELS: Record<EnvelopeField, string> = { name: \"From\", email: \"Email\", subject: \"Subject\", message: \"Message\" }\n\nconst FLY: Record<EnvelopeFlyDirection, { x: number; y: number; r: number }> = {\n  \"up-right\": { x: 1.25, y: -1.5, r: 16 },\n  up: { x: 0.04, y: -1.9, r: -5 },\n  right: { x: 1.7, y: -0.12, r: 9 },\n}\n\nconst firstName = (name: string) => name.trim().split(/\\s+/)[0] ?? \"\"\n\nconst defaultTitle = (v: EnvelopeValues) => (v.name ? `Sent. Thanks, ${firstName(v.name)}.` : \"Sent. Thank you.\")\nconst defaultMessage = \"Your letter is on its way. We'll write back soon.\"\n\ntype Phase = \"idle\" | \"sending\" | \"success\"\n\nexport function EnvelopeComposer({\n  variant = \"classic\",\n  color,\n  paper,\n  ink,\n  seal,\n  accent = ACCENT,\n  sealLabel,\n  fields,\n  placeholders,\n  labels,\n  messageMaxLength = 500,\n  sendLabel = \"Send\",\n  againLabel = \"Write another\",\n  successTitle = defaultTitle,\n  successMessage = defaultMessage,\n  errorMessage = \"Couldn't send. Try again.\",\n  onSend,\n  flyDirection = \"up-right\",\n  duration = 1,\n  width = 420,\n  radius = 14,\n  resetAfter = false,\n  className,\n}: EnvelopeComposerProps) {\n  const id = useId()\n  const reduced = useReducedMotion()\n  const p = palette(variant, { color, paper, ink, seal, accent })\n  const show = { name: fields?.name ?? true, email: fields?.email ?? true, subject: fields?.subject ?? false }\n  const ph = { ...DEFAULT_PLACEHOLDERS, ...placeholders }\n  const lb = { ...DEFAULT_LABELS, ...labels }\n  const speed = 1 / Math.max(0.1, duration)\n\n  const rootRef = useRef<HTMLDivElement>(null)\n  const flyRef = useRef<HTMLFormElement>(null)\n  const letterRef = useRef<HTMLDivElement>(null)\n  const creaseRef = useRef<HTMLDivElement>(null)\n  const footerRef = useRef<HTMLDivElement>(null)\n  const trailRef = useRef<HTMLDivElement>(null)\n  const flapRef = useRef<HTMLDivElement>(null)\n  const flapInnerRef = useRef<HTMLDivElement>(null)\n  const sealRef = useRef<HTMLDivElement>(null)\n  const againRef = useRef<HTMLButtonElement>(null)\n  const fieldRefs = useRef<Partial<Record<EnvelopeField, HTMLInputElement | HTMLTextAreaElement | null>>>({})\n  const [paperScope, animatePaper] = useAnimate<HTMLDivElement>()\n\n  const [values, setValues] = useState<EnvelopeValues>(EMPTY)\n  const [errors, setErrors] = useState<Partial<Record<EnvelopeField, string>>>({})\n  const [phase, setPhase] = useState<Phase>(\"idle\")\n  const [failure, setFailure] = useState(\"\")\n  const [status, setStatus] = useState(\"\")\n  const [sent, setSent] = useState<EnvelopeValues>(EMPTY)\n\n  const busy = phase !== \"idle\"\n  const label = (sealLabel || firstName(values.name || sent.name).charAt(0) || \"T\").toUpperCase()\n  const count = values.message.length\n  const firstError = ([\"name\", \"email\", \"subject\", \"message\"] as const).find((f) => errors[f])\n\n  const { contextSafe } = useGSAP(\n    () => {\n      gsap.set(flapRef.current, { rotationX: 180, transformPerspective: 900, transformOrigin: \"50% 0%\", zIndex: 10 })\n      gsap.set(letterRef.current, { transformOrigin: \"50% 100%\", transformPerspective: 1100 })\n      gsap.set(sealRef.current, { opacity: 0 })\n    },\n    { scope: rootRef }\n  )\n\n  /** How far the letter travels to sit folded at the bottom of the pocket. */\n  const measure = () => {\n    const letter = letterRef.current\n    const env = letter?.nextElementSibling as HTMLElement | null\n    if (!letter || !env) return { drop: 0, envH: 0, w: 0, h: 0 }\n    const overlap = letter.offsetTop + letter.offsetHeight - env.offsetTop\n    const envH = env.offsetHeight\n    return { drop: envH * 0.94 - overlap, envH, w: flyRef.current?.offsetWidth ?? 0, h: flyRef.current?.offsetHeight ?? 0 }\n  }\n\n  const runClose = contextSafe(() => {\n    const { drop } = measure()\n    const tl = gsap.timeline({ defaults: { ease: \"power4.inOut\" } }).timeScale(speed)\n    tl.to(footerRef.current, { opacity: 0, y: 6, duration: 0.25, ease: \"power2.out\" }, 0)\n      .to(letterRef.current, { scaleY: 0.64, rotationX: 22, duration: 0.36 }, 0.05)\n      .to(creaseRef.current, { opacity: 1, duration: 0.25 }, 0.05)\n      .to(letterRef.current, { scaleY: FOLD, rotationX: 0, duration: 0.34 }, \">-0.04\")\n      .to(letterRef.current, { y: drop, duration: 0.55, ease: \"power3.in\" }, \">-0.06\")\n      .addLabel(\"flap\", \">-0.08\")\n      .to(flapRef.current, { rotationX: 0, duration: 0.62 }, \"flap\")\n      .set(flapRef.current, { zIndex: 30 }, \"flap+=0.31\")\n      .set(flapInnerRef.current, { opacity: 0 }, \"flap+=0.31\")\n    return tl\n  })\n\n  const runFly = contextSafe((onDone: () => void) => {\n    const { w, h } = measure()\n    const v = FLY[flyDirection]\n    const x = w * v.x\n    const y = h * v.y\n    const angle = (Math.atan2(y, x) * 180) / Math.PI\n    const postmark = sealRef.current?.querySelector(\"[data-postmark]\")\n    const tl = gsap.timeline().timeScale(speed)\n    tl.fromTo(\n      sealRef.current,\n      { opacity: 0, scale: 1.9, y: -34, rotation: -14 },\n      { opacity: 1, scale: 1, y: 0, rotation: 0, duration: 0.32, ease: \"power4.in\" }\n    )\n      .to(sealRef.current, { scaleX: 1.18, scaleY: 0.82, duration: 0.08, ease: \"power2.out\" })\n      .to(sealRef.current, { scaleX: 1, scaleY: 1, duration: 0.5, ease: \"elastic.out(1, 0.45)\" })\n    if (postmark) {\n      tl.fromTo(postmark, { opacity: 0, scale: 1.25 }, { opacity: 1, scale: 1, duration: 0.18, ease: \"power4.in\" }, \"-=0.45\")\n    }\n    tl.to(flyRef.current, { rotation: -5, y: 10, scale: 0.97, duration: 0.32, ease: \"power2.inOut\" }, \"+=0.12\")\n      .addLabel(\"fly\")\n      .to(flyRef.current, { x, y, rotation: v.r, scale: 0.55, opacity: 0, filter: \"blur(6px)\", duration: 0.75, ease: \"power4.in\" }, \"fly\")\n      .set(trailRef.current, { xPercent: -100, yPercent: -50, transformOrigin: \"100% 50%\", rotation: angle }, \"fly\")\n      .fromTo(trailRef.current, { opacity: 0, scaleX: 0 }, { opacity: 0.85, scaleX: 1, duration: 0.4, ease: \"power2.out\" }, \"fly+=0.2\")\n      .to(trailRef.current, { opacity: 0, duration: 0.3 }, \">-0.05\")\n      .call(onDone, [], \"fly+=0.62\")\n    return tl\n  })\n\n  const runReopen = contextSafe((tl: gsap.core.Timeline) => {\n    return new Promise<void>((resolve) => {\n      if (tl.progress() === 0) {\n        tl.kill()\n        resolve()\n        return\n      }\n      tl.eventCallback(\"onReverseComplete\", () => resolve())\n      tl.timeScale(speed * 1.5).reverse()\n    })\n  })\n\n  const runAgain = contextSafe(() => {\n    const { drop, envH } = measure()\n    const focus = () => fieldRefs.current[show.name ? \"name\" : \"message\"]?.focus()\n    gsap.killTweensOf([flyRef.current, letterRef.current, flapRef.current, sealRef.current, trailRef.current])\n    gsap.set(flapRef.current, { rotationX: 180, zIndex: 10 })\n    gsap.set(flapInnerRef.current, { opacity: 1 })\n    gsap.set([sealRef.current, trailRef.current], { opacity: 0 })\n    if (reduced) {\n      gsap.set(letterRef.current, { y: 0, scaleY: 1, rotationX: 0 })\n      gsap.set([creaseRef.current], { opacity: 0 })\n      gsap.set(footerRef.current, { opacity: 1, y: 0 })\n      gsap.set(flyRef.current, { x: 0, y: 0, rotation: 0, scale: 1, clearProps: \"filter\" })\n      gsap.to(flyRef.current, { opacity: 1, duration: 0.25, onComplete: focus })\n      return\n    }\n    gsap.set(letterRef.current, { y: drop, scaleY: FOLD, rotationX: 0 })\n    gsap.set(creaseRef.current, { opacity: 1 })\n    gsap.set(footerRef.current, { opacity: 0, y: 6 })\n    gsap\n      .timeline({ onComplete: focus })\n      .timeScale(speed)\n      .fromTo(\n        flyRef.current,\n        { x: 0, y: envH * 0.7, rotation: -4, scale: 0.92, opacity: 0, filter: \"blur(6px)\" },\n        { y: 0, rotation: 0, scale: 1, opacity: 1, filter: \"blur(0px)\", duration: 0.8, ease: \"expo.out\" }\n      )\n      .set(flyRef.current, { clearProps: \"filter\" })\n      .to(letterRef.current, { y: 0, duration: 0.6, ease: \"expo.out\" }, 0.35)\n      .to(letterRef.current, { scaleY: 1, duration: 0.55, ease: \"expo.out\" }, 0.6)\n      .to(creaseRef.current, { opacity: 0, duration: 0.35 }, 0.7)\n      .to(footerRef.current, { opacity: 1, y: 0, duration: 0.45, ease: \"expo.out\" }, 0.8)\n  })\n\n  const runHide = contextSafe((onDone: () => void) => {\n    gsap.to(flyRef.current, { opacity: 0, duration: 0.25, onComplete: onDone })\n  })\n\n  const shake = () => {\n    if (!paperScope.current || reduced) return\n    animatePaper(paperScope.current, { x: [0, -10, 9, -6, 4, -2, 0] }, { duration: 0.5, ease: \"easeOut\" })\n  }\n\n  const validate = (v: EnvelopeValues) => {\n    const next: Partial<Record<EnvelopeField, string>> = {}\n    if (show.name && !v.name) next.name = \"Add your name\"\n    if (show.email && v.email && !EMAIL_RE.test(v.email)) next.email = \"Check the email address\"\n    if (!v.message) next.message = \"Write a message first\"\n    return next\n  }\n\n  const send = async () => {\n    if (busy) return\n    const snapshot: EnvelopeValues = {\n      name: show.name ? values.name.trim() : \"\",\n      email: show.email ? values.email.trim() : \"\",\n      subject: show.subject ? values.subject.trim() : \"\",\n      message: values.message.trim(),\n    }\n    const invalid = validate(snapshot)\n    const firstInvalid = ([\"name\", \"email\", \"subject\", \"message\"] as const).find((f) => invalid[f])\n    if (firstInvalid) {\n      setErrors(invalid)\n      setStatus(invalid[firstInvalid] ?? \"\")\n      shake()\n      fieldRefs.current[firstInvalid]?.focus()\n      return\n    }\n\n    setErrors({})\n    setFailure(\"\")\n    setPhase(\"sending\")\n    setStatus(\"Sending your message…\")\n\n    const finish = () => {\n      setSent(snapshot)\n      setPhase(\"success\")\n      setStatus(\"Message sent.\")\n    }\n    const fail = (err: unknown) => {\n      const text = err instanceof Error && err.message ? err.message : errorMessage\n      setPhase(\"idle\")\n      setFailure(text)\n      setStatus(text)\n      shake()\n    }\n\n    const request = Promise.resolve().then(() => onSend?.(snapshot))\n\n    if (reduced) {\n      try {\n        await request\n        runHide(finish)\n      } catch (err) {\n        fail(err)\n      }\n      return\n    }\n\n    const tl = runClose()\n    const closed = new Promise<void>((resolve) => {\n      tl.eventCallback(\"onComplete\", () => resolve())\n    })\n    try {\n      await Promise.all([request, closed])\n    } catch (err) {\n      await runReopen(tl)\n      gsap.to(footerRef.current, { opacity: 1, y: 0, duration: 0.3 })\n      fail(err)\n      return\n    }\n    runFly(finish)\n  }\n\n  const again = () => {\n    setValues(EMPTY)\n    setErrors({})\n    setFailure(\"\")\n    setStatus(\"\")\n    setPhase(\"idle\")\n    runAgain()\n  }\n\n  const autoReset = useEffectEvent(() => again())\n  useEffect(() => {\n    if (phase !== \"success\" || resetAfter === false) return\n    const t = setTimeout(autoReset, resetAfter)\n    return () => clearTimeout(t)\n  }, [phase, resetAfter])\n\n  const update = (field: EnvelopeField) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {\n    const next = e.target.value\n    setValues((v) => ({ ...v, [field]: next }))\n    if (errors[field]) setErrors((er) => ({ ...er, [field]: undefined }))\n    if (failure) setFailure(\"\")\n  }\n\n  const onKeyDown = (e: React.KeyboardEvent<HTMLFormElement>) => {\n    if (e.key !== \"Enter\") return\n    if (e.metaKey || e.ctrlKey) {\n      e.preventDefault()\n      void send()\n    } else if (e.target instanceof HTMLInputElement) {\n      e.preventDefault()\n    }\n  }\n\n  const input = (field: Exclude<EnvelopeField, \"message\">, type = \"text\", autoComplete?: string) => (\n    <label className=\"flex min-w-0 flex-col gap-1\">\n      <span className=\"text-[10px] font-medium uppercase tracking-[0.14em]\" style={{ color: p.muted }}>\n        {lb[field]}\n      </span>\n      <input\n        ref={(el) => {\n          fieldRefs.current[field] = el\n        }}\n        id={`${id}-${field}`}\n        type={type}\n        value={values[field]}\n        onChange={update(field)}\n        placeholder={ph[field]}\n        autoComplete={autoComplete}\n        disabled={busy}\n        aria-invalid={errors[field] ? true : undefined}\n        aria-describedby={errors[field] ? `${id}-error` : undefined}\n        className=\"w-full min-w-0 border-b border-(--env-crease) bg-transparent pb-1 text-sm outline-none transition-colors placeholder:opacity-50 focus:border-(--env-accent) disabled:opacity-100 aria-invalid:border-(--env-error)\"\n        style={{ color: p.ink, caretColor: p.accent }}\n      />\n    </label>\n  )\n\n  const title = typeof successTitle === \"function\" ? successTitle(sent) : successTitle\n  const body = typeof successMessage === \"function\" ? successMessage(sent) : successMessage\n\n  return (\n    <div ref={rootRef} className={cn(\"@container relative isolate mx-auto\", className)} style={vars(p, { width, maxWidth: \"100%\" })}>\n      {p.glass && <GlassGlow accent={p.accent} />}\n\n      <form\n        ref={flyRef}\n        noValidate\n        aria-label=\"Write a message\"\n        inert={phase === \"success\"}\n        onSubmit={(e) => {\n          e.preventDefault()\n          void send()\n        }}\n        onKeyDown={onKeyDown}\n        className=\"relative isolate\"\n      >\n        {/* letter */}\n        <div ref={letterRef} className=\"relative z-20 mx-auto w-[86%]\" style={{ marginBottom: \"-9%\" }}>\n          <div\n            ref={paperScope}\n            className={cn(\"relative overflow-hidden px-4 pt-4\", p.font)}\n            style={{\n              backgroundColor: p.paper,\n              color: p.ink,\n              borderRadius: Math.round(radius * 0.6),\n              boxShadow: shadowOf(p),\n              paddingBottom: \"14%\",\n              backdropFilter: p.glass ? \"blur(12px)\" : undefined,\n            }}\n          >\n            {(show.name || show.email) && (\n              <div className={cn(\"mb-3 grid gap-3\", show.name && show.email && \"@[22rem]:grid-cols-2\")}>\n                {show.name && input(\"name\", \"text\", \"name\")}\n                {show.email && input(\"email\", \"email\", \"email\")}\n              </div>\n            )}\n            {show.subject && <div className=\"mb-3\">{input(\"subject\")}</div>}\n\n            <label htmlFor={`${id}-message`} className=\"sr-only\">\n              {lb.message}\n            </label>\n            <textarea\n              ref={(el) => {\n                fieldRefs.current.message = el\n              }}\n              id={`${id}-message`}\n              value={values.message}\n              onChange={update(\"message\")}\n              placeholder={ph.message}\n              maxLength={messageMaxLength}\n              rows={3}\n              disabled={busy}\n              aria-invalid={errors.message ? true : undefined}\n              aria-describedby={`${id}-count${errors.message ? ` ${id}-error` : \"\"}`}\n              className=\"block w-full resize-none bg-transparent text-sm leading-6 outline-none placeholder:opacity-50 disabled:opacity-100\"\n              style={{\n                color: p.ink,\n                caretColor: p.accent,\n                // Hard-edged ruled lines (a pattern, no color blending)\n                backgroundImage: `repeating-linear-gradient(to bottom, transparent 0 23px, ${errors.message ? ERROR : p.crease} 23px 24px)`,\n                backgroundAttachment: \"local\",\n              }}\n            />\n            <div className=\"mt-1.5 flex items-center justify-between gap-3 font-sans text-[11px]\">\n              <AnimatePresence mode=\"wait\" initial={false}>\n                {firstError ? (\n                  <motion.span\n                    key={firstError}\n                    id={`${id}-error`}\n                    initial={{ opacity: 0, y: 4 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    exit={{ opacity: 0, y: -4 }}\n                    transition={{ duration: 0.2, ease: EASE_OUT }}\n                    style={{ color: ERROR }}\n                  >\n                    {errors[firstError]}\n                  </motion.span>\n                ) : (\n                  <span />\n                )}\n              </AnimatePresence>\n              <span\n                id={`${id}-count`}\n                className=\"tabular-nums transition-colors\"\n                style={{ color: count > messageMaxLength * 0.9 ? p.accent : p.muted }}\n                aria-label={`${count} of ${messageMaxLength} characters`}\n              >\n                {count}/{messageMaxLength}\n              </span>\n            </div>\n            <div\n              ref={creaseRef}\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute inset-0 opacity-0\"\n            >\n              <CreaseLines />\n            </div>\n          </div>\n        </div>\n\n        <EnvelopeBody\n          p={p}\n          radius={radius}\n          sealLabel={label}\n          flapOpen\n          sealShown={false}\n          flapRef={flapRef}\n          flapInnerRef={flapInnerRef}\n          sealRef={sealRef}\n          front={\n            <div ref={footerRef} className=\"absolute inset-x-[5%] bottom-[9%] flex items-center justify-between gap-2 font-sans\">\n              <AnimatePresence mode=\"wait\" initial={false}>\n                {failure ? (\n                  <motion.span\n                    key=\"fail\"\n                    role=\"alert\"\n                    initial={{ opacity: 0, y: 4 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    exit={{ opacity: 0 }}\n                    className=\"min-w-0 truncate rounded-full px-2 py-0.5 text-[11px] font-medium\"\n                    style={{ color: ERROR, background: mix(ERROR, 12, \"transparent\") }}\n                  >\n                    {failure}\n                  </motion.span>\n                ) : (\n                  <motion.span\n                    key=\"hint\"\n                    initial={{ opacity: 0 }}\n                    animate={{ opacity: 1 }}\n                    exit={{ opacity: 0 }}\n                    className=\"hidden items-center gap-1 text-[11px] @[20rem]:flex\"\n                    style={{ color: p.frontInk }}\n                  >\n                    <kbd className=\"rounded border px-1 font-mono text-[10px]\" style={{ borderColor: p.crease }}>\n                      ⌘/Ctrl\n                    </kbd>\n                    <kbd className=\"rounded border px-1 font-mono text-[10px]\" style={{ borderColor: p.crease }}>\n                      Enter\n                    </kbd>\n                    <span>to send</span>\n                  </motion.span>\n                )}\n              </AnimatePresence>\n              <motion.button\n                type=\"submit\"\n                disabled={busy}\n                whileHover={reduced || busy ? undefined : { scale: 1.04, y: -1 }}\n                whileTap={reduced || busy ? undefined : { scale: 0.96 }}\n                transition={spring}\n                className=\"ml-auto inline-flex h-8 shrink-0 items-center gap-1.5 rounded-full px-3.5 text-[13px] font-medium text-white outline-none focus-visible:ring-2 focus-visible:ring-(--env-accent)/40 focus-visible:ring-offset-2 disabled:cursor-default\"\n                style={{ background: p.accent, boxShadow: `0 8px 20px -8px ${mix(p.accent, 80, \"transparent\")}` }}\n              >\n                {sendLabel}\n                <Send className=\"size-3.5\" aria-hidden=\"true\" />\n              </motion.button>\n            </div>\n          }\n        >\n          <div\n            ref={trailRef}\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute left-1/2 top-1/2 -z-10 h-[6%] w-[130%] rounded-full opacity-0\"\n            style={{ backgroundColor: mix(p.accent, 35, \"transparent\") }}\n          />\n        </EnvelopeBody>\n      </form>\n\n      <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {status}\n      </div>\n\n      <AnimatePresence>\n        {phase === \"success\" && (\n          <motion.div\n            key=\"done\"\n            className=\"absolute inset-0 z-50 grid place-items-center text-center\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0, transition: { duration: 0.2 } }}\n            transition={{ duration: reduced ? 0.2 : 0.3 }}\n            onAnimationComplete={() => againRef.current?.focus({ preventScroll: true })}\n          >\n            <motion.div\n              className=\"flex max-w-[82%] flex-col items-center gap-3\"\n              initial={reduced ? { opacity: 0 } : { opacity: 0, y: 14, filter: \"blur(6px)\" }}\n              animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n              transition={{ duration: reduced ? 0.2 : 0.6, ease: EASE_OUT }}\n            >\n              <motion.span\n                initial={reduced ? false : { scale: 0.4, rotate: -20 }}\n                animate={{ scale: 1, rotate: 0 }}\n                transition={spring}\n                className=\"grid size-12 place-items-center rounded-full text-white\"\n                style={{ background: p.accent, boxShadow: `0 12px 30px -8px ${mix(p.accent, 70, \"transparent\")}` }}\n              >\n                <Check className=\"size-6\" strokeWidth={2.6} aria-hidden=\"true\" />\n              </motion.span>\n              <h3 className=\"text-xl font-semibold tracking-tight text-foreground\">{title}</h3>\n              <p className=\"text-sm text-muted-foreground\">{body}</p>\n              <motion.button\n                ref={againRef}\n                type=\"button\"\n                onClick={again}\n                whileHover={reduced ? undefined : { scale: 1.03 }}\n                whileTap={reduced ? undefined : { scale: 0.97 }}\n                transition={spring}\n                className=\"mt-2 inline-flex h-9 items-center gap-2 rounded-full border bg-panel px-4 text-[13px] font-medium text-foreground shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n              >\n                <PenLine className=\"size-3.5\" aria-hidden=\"true\" />\n                {againLabel}\n              </motion.button>\n            </motion.div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  )\n}\n\n/* ------------------------------------------------------------ reveal only */\n\nexport function Envelope({\n  children,\n  variant = \"classic\",\n  color,\n  paper,\n  ink,\n  seal,\n  accent = ACCENT,\n  sealLabel = \"T\",\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  trigger = \"click\",\n  label = \"Open envelope\",\n  duration = 1,\n  width = 420,\n  radius = 14,\n  className,\n  letterClassName,\n}: EnvelopeProps) {\n  const reduced = useReducedMotion()\n  const p = palette(variant, { color, paper, ink, seal, accent })\n  const speed = 1 / Math.max(0.1, duration)\n\n  const rootRef = useRef<HTMLDivElement>(null)\n  const envRef = useRef<HTMLDivElement>(null)\n  const letterRef = useRef<HTMLDivElement>(null)\n  const creaseRef = useRef<HTMLDivElement>(null)\n  const flapRef = useRef<HTMLDivElement>(null)\n  const flapInnerRef = useRef<HTMLDivElement>(null)\n  const sealRef = useRef<HTMLDivElement>(null)\n  const tlRef = useRef<gsap.core.Timeline | null>(null)\n\n  const [inner, setInner] = useState(defaultOpen)\n  const inView = useInView(rootRef, { once: true, amount: 0.6 })\n  const isOpen = openProp ?? (trigger === \"inView\" ? inner || inView : inner)\n  const openRef = useRef(isOpen)\n  const shownRef = useRef(false)\n\n  const setOpen = (next: boolean) => {\n    if (next === isOpen) return\n    if (openProp === undefined) setInner(next)\n    onOpenChange?.(next)\n  }\n\n  const announceInView = useEffectEvent(() => {\n    if (openProp === undefined && !inner) onOpenChange?.(true)\n  })\n  useEffect(() => {\n    if (trigger === \"inView\" && inView) announceInView()\n  }, [trigger, inView])\n\n  // Build the open timeline (paused), then park it at the current state.\n  useGSAP(\n    () => {\n      const letter = letterRef.current\n      const env = envRef.current\n      if (!letter || !env) return\n      const envH = env.offsetHeight\n      const h = Math.max(1, letter.offsetHeight)\n      const closed = Math.min(FOLD, (envH * 0.85) / h)\n      // Open: the letter's tucked bottom stays in the pocket (22% deep)\n      const rest = -envH * 0.72\n\n      gsap.set(flapRef.current, { rotationX: 0, transformPerspective: 900, transformOrigin: \"50% 0%\", zIndex: 30 })\n      gsap.set(flapInnerRef.current, { opacity: 0 })\n      gsap.set(letter, { transformOrigin: \"50% 100%\", scaleY: closed, y: 0 })\n      gsap.set(creaseRef.current, { opacity: 1 })\n      gsap.set(sealRef.current, { opacity: 1, scale: 1, y: 0 })\n\n      const tl = gsap.timeline({ paused: true, defaults: { ease: \"power4.inOut\" } })\n      tl.to(sealRef.current, { scale: 1.12, duration: 0.12, ease: \"power2.out\" })\n        .to(sealRef.current, { scale: 0.5, y: -8, opacity: 0, duration: 0.26, ease: \"power4.in\" })\n        .addLabel(\"flap\", \"-=0.05\")\n        .to(flapRef.current, { rotationX: 180, duration: 0.62 }, \"flap\")\n        .set(flapRef.current, { zIndex: 10 }, \"flap+=0.31\")\n        .set(flapInnerRef.current, { opacity: 1 }, \"flap+=0.31\")\n        .to(letter, { y: rest, duration: 0.8, ease: \"expo.out\" }, \"flap+=0.4\")\n        .to(letter, { scaleY: 1, duration: 0.7, ease: \"expo.out\" }, \"flap+=0.55\")\n        .to(creaseRef.current, { opacity: 0, duration: 0.45, ease: \"power2.out\" }, \"<0.15\")\n      // Reserve exactly as much room above as the open letter actually sticks out,\n      // measured from the finished pose so the layout never jumps or overflows\n      tl.progress(1)\n      const protrude = env.getBoundingClientRect().top - letter.getBoundingClientRect().top\n      // Assigned directly: gsap would keep the initial % unit and read 178px as 178%\n      if (rootRef.current) rootRef.current.style.paddingTop = `${Math.max(12, Math.round(protrude + 12))}px`\n      tl.timeScale(speed).progress(openRef.current ? 1 : 0)\n      tlRef.current = tl\n      return () => {\n        tlRef.current = null\n      }\n    },\n    { scope: rootRef, dependencies: [speed, variant], revertOnUpdate: true }\n  )\n\n  useGSAP(\n    () => {\n      openRef.current = isOpen\n      const tl = tlRef.current\n      if (!tl) return\n      if (!shownRef.current || reduced) {\n        shownRef.current = true\n        tl.pause().progress(isOpen ? 1 : 0)\n        return\n      }\n      tl.timeScale(isOpen ? speed : speed * 1.4)\n      if (isOpen) tl.play()\n      else tl.reverse()\n    },\n    { dependencies: [isOpen] }\n  )\n\n  const hover = trigger === \"hover\"\n  const interactive = trigger === \"click\" || trigger === \"hover\"\n\n  return (\n    <div\n      ref={rootRef}\n      className={cn(\"@container relative isolate mx-auto\", className)}\n      style={vars(p, { width, maxWidth: \"100%\", paddingTop: \"36%\" })}\n      onPointerEnter={hover ? () => setOpen(true) : undefined}\n      onPointerLeave={hover ? () => setOpen(false) : undefined}\n    >\n      {p.glass && <GlassGlow accent={p.accent} />}\n      <motion.div\n        ref={envRef}\n        whileHover={interactive && !isOpen && !reduced ? { y: -4, rotate: -0.6 } : undefined}\n        transition={spring}\n        className=\"relative\"\n      >\n        <EnvelopeBody\n          p={p}\n          radius={radius}\n          sealLabel={sealLabel}\n          flapOpen={false}\n          sealShown\n          flapRef={flapRef}\n          flapInnerRef={flapInnerRef}\n          sealRef={sealRef}\n        >\n          <div\n            ref={letterRef}\n            className=\"absolute inset-x-[5%] bottom-[6%] z-20\"\n            style={{ transform: \"scaleY(0.34)\", transformOrigin: \"50% 100%\" }}\n          >\n            <div\n              className={cn(\"relative overflow-hidden p-5\", p.font, letterClassName)}\n              style={{\n                paddingBottom: \"calc(1.25rem + 14%)\",\n                backgroundColor: p.paper,\n                  color: p.ink,\n                borderRadius: Math.round(radius * 0.6),\n                boxShadow: shadowOf(p),\n                backdropFilter: p.glass ? \"blur(12px)\" : undefined,\n              }}\n              aria-hidden={!isOpen}\n              inert={!isOpen}\n            >\n              {children}\n              <div\n                ref={creaseRef}\n                aria-hidden=\"true\"\n                className=\"pointer-events-none absolute inset-0\"\n              >\n                <CreaseLines />\n              </div>\n            </div>\n          </div>\n          {interactive && (\n            <button\n              type=\"button\"\n              aria-expanded={isOpen}\n              aria-label={label}\n              onClick={() => setOpen(!isOpen)}\n              className=\"absolute inset-0 z-[35] cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-(--env-accent) focus-visible:ring-offset-2 focus-visible:ring-offset-transparent\"\n              style={{ borderRadius: radius }}\n            />\n          )}\n        </EnvelopeBody>\n      </motion.div>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Docs, live playground and every prop: https://trytweenly.vercel.app/docs/components/envelope-reveal",
  "type": "registry:component"
}