{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"gift-wrap","type":"registry:component","title":"Gift wrap","description":"Folds kraft paper over the cart item, ties a ribbon and bow, and asks who it's for.","author":"Yash Bavadiya <https://xevrion.dev>","dependencies":["motion"],"registryDependencies":["utils"],"files":[{"path":"components/gift-wrap.tsx","type":"registry:component","content":"\"use client\";\n\nimport { useId, useState } from \"react\";\nimport { AnimatePresence, motion, type Variants } from \"motion/react\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { cn } from \"@/lib/utils\";\n\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst ROLL = { type: \"spring\", duration: 0.35, bounce: 0 } as const;\n\n// Wrapping paper and ribbon are physical materials, so they keep their own\n// colours in both themes: brown kraft paper with a printed dot, and a red\n// satin ribbon with a darker underside where it folds.\nconst KRAFT = \"oklch(0.74 0.075 70)\";\nconst KRAFT_FOLD = \"oklch(0.64 0.075 65)\";\nconst KRAFT_DOT = \"oklch(0.97 0.02 80 / 0.75)\";\nconst RIBBON = \"oklch(0.55 0.19 25)\";\nconst RIBBON_DARK = \"oklch(0.44 0.17 25)\";\n// Satin catches light down its middle.\nconst RIBBON_SHEEN = \"oklch(0.66 0.17 28)\";\nconst SATIN = (angle: number) =>\n  `linear-gradient(${angle}deg, ${RIBBON_DARK}, ${RIBBON} 25%, ${RIBBON_SHEEN} 50%, ${RIBBON} 75%, ${RIBBON_DARK})`;\n// Paper pulled over a box: light on the top edge, shade toward the bottom.\nconst PILLOW =\n  \"linear-gradient(180deg, oklch(1 0 0 / 0.16), transparent 35%, transparent 65%, oklch(0 0 0 / 0.12))\";\n// A plain card tag, like the paper ones tied to a real gift.\nconst TAG = \"oklch(0.97 0.012 85)\";\nconst TAG_INK = \"oklch(0.3 0.02 60)\";\n\n// Wrapping is a rare, one-off moment, so the whole sequence (paper, ribbon,\n// bow, tag) takes about 0.8s; each step on its own stays under 300ms.\n// Unwrapping runs the same steps backwards in about half the time, since\n// the user is undoing rather than watching.\nconst flap = (side: 1 | -1, delay: number): Variants => ({\n  // Hovering the gift option lifts the paper in from both edges, a promise\n  // of what the switch does before it is pressed.\n  peek: {\n    // 87deg shows about 14px of paper (perspective widens it): inside the\n    // row's padding, so the price and photo stay readable while it teases.\n    rotateY: side * 87,\n    opacity: 1,\n    transition: {\n      rotateY: { type: \"spring\", duration: 0.35, bounce: 0 },\n      opacity: { duration: 0.06 },\n    },\n  },\n  open: {\n    rotateY: side * 92,\n    opacity: 0,\n    transition: {\n      rotateY: { duration: 0.16, ease: EASE_OUT, delay: 0.12 },\n      opacity: { duration: 0.06, delay: 0.22 },\n    },\n  },\n  wrapped: {\n    rotateY: 0,\n    opacity: 1,\n    transition: {\n      rotateY: { duration: 0.3, ease: EASE_OUT, delay },\n      opacity: { duration: 0.06, delay },\n    },\n  },\n});\n\nconst ribbon = (axis: \"scaleX\" | \"scaleY\", delay: number): Variants => ({\n  open: {\n    ...(axis === \"scaleX\" ? { scaleX: 0 } : { scaleY: 0 }),\n    transition: { duration: 0.1, ease: EASE_OUT, delay: 0.06 },\n  },\n  wrapped: {\n    ...(axis === \"scaleX\" ? { scaleX: 1 } : { scaleY: 1 }),\n    transition: { duration: 0.18, ease: EASE_OUT, delay },\n  },\n});\n\nconst BOW: Variants = {\n  open: {\n    scale: 0.4,\n    opacity: 0,\n    transition: { duration: 0.08, ease: EASE_OUT },\n  },\n  // The one bouncy thing here: a bow pulled tight springs a little.\n  wrapped: {\n    scale: 1,\n    opacity: 1,\n    transition: {\n      scale: { type: \"spring\", duration: 0.4, bounce: 0.45, delay: 0.56 },\n      opacity: { duration: 0.08, delay: 0.56 },\n    },\n  },\n};\n\nconst TAG_SWING: Variants = {\n  open: { opacity: 0, rotate: -2, y: -4, transition: { duration: 0.08 } },\n  wrapped: {\n    opacity: 1,\n    rotate: -9,\n    y: 0,\n    transition: {\n      default: { type: \"spring\", duration: 0.5, bounce: 0.35, delay: 0.66 },\n      opacity: { duration: 0.12, delay: 0.66 },\n    },\n  },\n};\n\nconst FADE_ONLY: Variants = {\n  open: { opacity: 0, transition: { duration: 0.12 } },\n  wrapped: { opacity: 1, transition: { duration: 0.2 } },\n};\n\nconst money = (cents: number, currency: string) =>\n  new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency }).format(cents / 100);\n\nexport type GiftItem = {\n  name: string;\n  variant: string;\n  // In cents, so totals never pick up float error.\n  price: number;\n  image: React.ReactNode;\n};\n\nexport function GiftWrap({\n  item,\n  wrapFee,\n  currency = \"USD\",\n  maxMessage = 150,\n  defaultGift = false,\n  onChange,\n  className,\n}: {\n  item: GiftItem;\n  // In cents.\n  wrapFee: number;\n  currency?: string;\n  maxMessage?: number;\n  defaultGift?: boolean;\n  onChange?: (gift: { on: boolean; to: string; message: string }) => void;\n  className?: string;\n}) {\n  const reduceMotion = useReducedMotion();\n  const id = useId();\n  const [gift, setGift] = useState(defaultGift);\n  const [to, setTo] = useState(\"\");\n  const [message, setMessage] = useState(\"\");\n  const [peek, setPeek] = useState(false);\n\n  const update = (next: Partial<{ on: boolean; to: string; message: string }>) => {\n    const state = { on: gift, to, message, ...next };\n    onChange?.(state);\n  };\n\n  const fee = gift ? wrapFee : 0;\n  const total = item.price + fee;\n  const v = (variants: Variants) => (reduceMotion ? FADE_ONLY : variants);\n\n  return (\n    <div\n      className={cn(\n        \"w-[min(440px,100%)] rounded-3xl bg-background p-5 shadow-raised\",\n        className,\n      )}\n    >\n      <div className=\"flex items-baseline justify-between\">\n        <h3 className=\"text-[15px] font-medium text-foreground\">Your bag</h3>\n        <p className=\"text-[13px] text-muted-foreground\">1 item</p>\n      </div>\n\n      {/* The product row. The paper folds shut over it from both edges, so\n          it needs perspective and a clip at the rounded corners. */}\n      <motion.div\n        initial={false}\n        animate={gift ? \"wrapped\" : peek && !reduceMotion ? \"peek\" : \"open\"}\n        className=\"relative mt-3 h-[104px] overflow-hidden rounded-2xl bg-muted [perspective:700px]\"\n      >\n        <div className=\"flex h-full items-center gap-4 p-4\">\n          {/* A product photo keeps its light studio backdrop in both themes,\n              the way the site's avatars keep their light circle. */}\n          <div className=\"grid size-[72px] shrink-0 place-items-center overflow-hidden rounded-xl bg-[oklch(0.97_0_0)] outline outline-1 -outline-offset-1 outline-black/5\">\n            {item.image}\n          </div>\n          <div className=\"min-w-0 flex-1\">\n            <p className=\"truncate text-[15px] font-medium text-foreground\">{item.name}</p>\n            <p className=\"truncate text-sm text-muted-foreground\">{item.variant}</p>\n            <p className=\"mt-1 text-sm text-muted-foreground\">Qty 1</p>\n          </div>\n          <p className=\"self-start text-[15px] text-foreground tabular-nums\">\n            {money(item.price, currency)}\n          </p>\n        </div>\n\n        <div aria-hidden className=\"pointer-events-none absolute inset-0\">\n          <Flap side={-1} variants={v(flap(-1, 0))} pattern={`${id}-l`} />\n          <Flap side={1} variants={v(flap(1, 0.08))} pattern={`${id}-r`} />\n          <motion.span\n            variants={v(ribbon(\"scaleY\", 0.3))}\n            style={{ background: SATIN(90), originY: 0 }}\n            className=\"absolute inset-y-0 left-[68%] z-20 w-3.5\"\n          />\n          <motion.span\n            variants={v(ribbon(\"scaleX\", 0.4))}\n            style={{ background: SATIN(180), originX: 0 }}\n            className=\"absolute inset-x-0 top-[calc(50%-7px)] z-20 h-3.5\"\n          />\n          <motion.div\n            variants={v(BOW)}\n            className=\"absolute top-[calc(50%-17px)] left-[calc(68%-19px)] z-30 h-[34px] w-[52px]\"\n          >\n            <Bow />\n          </motion.div>\n          <motion.div\n            variants={v(TAG_SWING)}\n            style={{ background: TAG, color: TAG_INK, originX: 0, originY: 0.5 }}\n            className=\"absolute top-[calc(50%+14px)] left-[calc(68%+22px)] z-20 flex h-7 max-w-[30%] items-center gap-1.5 rounded-[4px] pr-2 pl-3 text-[12px] font-medium shadow-[0_1px_2px_oklch(0_0_0/0.2)] [clip-path:polygon(8px_0,100%_0,100%_100%,8px_100%,0_50%)]\"\n          >\n            <span className=\"size-1 shrink-0 rounded-full bg-current opacity-40\" />\n            <span className=\"truncate\">For {to.trim() || \"you\"}</span>\n          </motion.div>\n        </div>\n      </motion.div>\n\n      <button\n        type=\"button\"\n        role=\"switch\"\n        aria-checked={gift}\n        onClick={() => {\n          setGift(!gift);\n          update({ on: !gift });\n        }}\n        onPointerEnter={(e) => e.pointerType !== \"touch\" && setPeek(true)}\n        onPointerLeave={() => setPeek(false)}\n        onFocus={(e) => e.currentTarget.matches(\":focus-visible\") && setPeek(true)}\n        onBlur={() => setPeek(false)}\n        className=\"group mt-3 flex min-h-11 w-full touch-manipulation items-center gap-3 rounded-2xl p-1 text-left outline-hidden select-none focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-foreground\"\n      >\n        <Swatch />\n        <span className=\"min-w-0 flex-1\">\n          <span className=\"block text-[15px] font-medium text-foreground\">This is a gift</span>\n          <span className=\"block text-[13px] text-muted-foreground\">\n            Kraft paper, satin bow and a note, +{money(wrapFee, currency)}\n          </span>\n        </span>\n        <span\n          aria-hidden\n          className={cn(\n            \"relative h-[26px] w-11 shrink-0 rounded-full transition-[background-color,scale] duration-200 ease-out group-active:scale-[0.96]\",\n            gift ? \"bg-foreground\" : \"bg-foreground/15\",\n          )}\n        >\n          <span\n            className={cn(\n              \"absolute top-[3px] left-[3px] size-5 rounded-full bg-background shadow-raised transition-transform duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none\",\n              gift && \"translate-x-[18px]\",\n            )}\n          />\n        </span>\n      </button>\n\n      {/* Unfolds downward like a card opening; the toggle above never moves. */}\n      <AnimatePresence initial={false}>\n        {gift && (\n          <motion.div\n            key=\"note\"\n            initial={{ height: 0, opacity: 0 }}\n            animate={{\n              height: \"auto\",\n              opacity: 1,\n              transition: {\n                height: { duration: reduceMotion ? 0 : 0.28, ease: EASE_OUT },\n                opacity: { duration: 0.2 },\n              },\n            }}\n            exit={{\n              height: 0,\n              opacity: 0,\n              transition: {\n                height: { duration: reduceMotion ? 0 : 0.18, ease: EASE_OUT },\n                opacity: { duration: 0.1 },\n              },\n            }}\n            className=\"overflow-hidden [perspective:600px]\"\n          >\n            <motion.div\n              initial={reduceMotion ? false : { rotateX: -70, filter: \"blur(4px)\" }}\n              animate={{ rotateX: 0, filter: \"blur(0px)\" }}\n              exit={reduceMotion ? undefined : { rotateX: -40, transition: { duration: 0.16, ease: EASE_OUT } }}\n              transition={{ duration: 0.32, ease: EASE_OUT }}\n              style={{ originY: 0 }}\n              className=\"flex flex-col gap-2 pt-2 pb-1\"\n            >\n              <label className=\"flex h-11 items-center gap-2 rounded-xl bg-muted px-3 focus-within:outline-2 focus-within:outline-solid focus-within:-outline-offset-2 focus-within:outline-foreground\">\n                <span className=\"text-sm text-muted-foreground\">To</span>\n                <input\n                  value={to}\n                  maxLength={24}\n                  onChange={(e) => {\n                    setTo(e.target.value);\n                    update({ to: e.target.value });\n                  }}\n                  placeholder=\"Their name\"\n                  className=\"h-full min-w-0 flex-1 bg-transparent text-[15px] text-foreground outline-hidden placeholder:text-muted-foreground/70\"\n                />\n              </label>\n              <div className=\"relative rounded-xl bg-muted focus-within:outline-2 focus-within:outline-solid focus-within:-outline-offset-2 focus-within:outline-foreground\">\n                <label htmlFor={`${id}-msg`} className=\"sr-only\">\n                  Gift message\n                </label>\n                <textarea\n                  id={`${id}-msg`}\n                  value={message}\n                  maxLength={maxMessage}\n                  rows={3}\n                  onChange={(e) => {\n                    setMessage(e.target.value);\n                    update({ message: e.target.value });\n                  }}\n                  placeholder=\"Write a note, we'll print it on a card\"\n                  aria-describedby={`${id}-count`}\n                  className=\"block w-full resize-none bg-transparent px-3 pt-2.5 pb-6 text-[15px] leading-snug text-foreground outline-hidden placeholder:text-muted-foreground/70\"\n                />\n                <span\n                  id={`${id}-count`}\n                  className={cn(\n                    \"pointer-events-none absolute right-3 bottom-2 text-[12px] tabular-nums transition-[color] duration-150\",\n                    maxMessage - message.length <= 10 ? \"text-destructive\" : \"text-muted-foreground\",\n                  )}\n                >\n                  {maxMessage - message.length} left\n                </span>\n              </div>\n            </motion.div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n\n      <dl className=\"mt-3 flex flex-col gap-1.5 border-t border-border pt-3 text-sm\">\n        <div className=\"flex justify-between\">\n          <dt className=\"text-muted-foreground\">Subtotal</dt>\n          <dd className=\"text-foreground tabular-nums\">{money(item.price, currency)}</dd>\n        </div>\n        <div className=\"flex justify-between\">\n          <dt className=\"text-muted-foreground\">Gift wrap</dt>\n          <dd className={cn(\"transition-[color] duration-200\", gift ? \"text-foreground\" : \"text-muted-foreground\")}>\n            <RollingNumber text={money(fee, currency)} value={fee} />\n          </dd>\n        </div>\n        <div className=\"mt-1 flex items-baseline justify-between text-[15px] font-medium\">\n          <dt className=\"text-foreground\">Total</dt>\n          <dd className=\"text-foreground\">\n            <RollingNumber text={money(total, currency)} value={total} />\n          </dd>\n        </div>\n      </dl>\n    </div>\n  );\n}\n\nfunction Flap({\n  side,\n  variants,\n  pattern,\n}: {\n  side: 1 | -1;\n  variants: Variants;\n  pattern: string;\n}) {\n  const left = side === -1;\n  return (\n    <motion.div\n      variants={variants}\n      style={{ originX: left ? 0 : 1, background: KRAFT }}\n      className={cn(\n        // Each half overlaps the middle by 6px, so the paper meets with a\n        // lap instead of a gap. The right flap folds over the left.\n        \"absolute inset-y-0 w-[calc(50%+6px)] backface-hidden\",\n        left ? \"left-0\" : \"right-0 z-10 shadow-[-2px_0_4px_oklch(0_0_0/0.18)]\",\n      )}\n    >\n      <svg className=\"absolute inset-0 size-full\">\n        <defs>\n          <pattern id={pattern} width=\"14\" height=\"14\" patternUnits=\"userSpaceOnUse\">\n            <circle cx=\"3.5\" cy=\"3.5\" r=\"1.4\" fill={KRAFT_DOT} />\n            <circle cx=\"10.5\" cy=\"10.5\" r=\"1.4\" fill={KRAFT_DOT} />\n          </pattern>\n        </defs>\n        <rect width=\"100%\" height=\"100%\" fill={`url(#${pattern})`} />\n      </svg>\n      <span style={{ background: PILLOW }} className=\"absolute inset-0\" />\n      {/* The folded-under edge, a strip of the paper's back showing. */}\n      {!left && (\n        <span\n          style={{ background: KRAFT_FOLD }}\n          className=\"absolute inset-y-0 left-0 w-[5px]\"\n        />\n      )}\n    </motion.div>\n  );\n}\n\n// A cut sample of the paper and ribbon, so the option shows what you get\n// before the switch is ever pressed.\nfunction Swatch() {\n  return (\n    <svg aria-hidden viewBox=\"0 0 44 44\" className=\"size-11 shrink-0 overflow-visible rounded-xl\">\n      <defs>\n        <pattern id=\"gift-swatch-dots\" width=\"7\" height=\"7\" patternUnits=\"userSpaceOnUse\">\n          <circle cx=\"1.75\" cy=\"1.75\" r=\"0.8\" fill={KRAFT_DOT} />\n          <circle cx=\"5.25\" cy=\"5.25\" r=\"0.8\" fill={KRAFT_DOT} />\n        </pattern>\n      </defs>\n      <rect width=\"44\" height=\"44\" rx=\"12\" fill={KRAFT} />\n      <rect width=\"44\" height=\"44\" rx=\"12\" fill=\"url(#gift-swatch-dots)\" />\n      <rect x=\"26\" width=\"5\" height=\"44\" fill={RIBBON} />\n      <rect y=\"19.5\" width=\"44\" height=\"5\" fill={RIBBON} />\n      <path d=\"M28.5 22 C24 15, 17 15, 18 20 C19 24, 25 23.5, 28.5 22Z\" fill={RIBBON} stroke={RIBBON_DARK} strokeWidth=\"0.8\" />\n      <path d=\"M28.5 22 C33 15, 40 15, 39 20 C38 24, 32 23.5, 28.5 22Z\" fill={RIBBON} stroke={RIBBON_DARK} strokeWidth=\"0.8\" />\n      <rect x=\"26.5\" y=\"19.5\" width=\"4\" height=\"5\" rx=\"1.5\" fill={RIBBON_DARK} />\n      <rect width=\"44\" height=\"44\" rx=\"12\" fill=\"none\" stroke=\"oklch(0 0 0 / 0.08)\" />\n    </svg>\n  );\n}\n\nfunction Bow() {\n  return (\n    <svg viewBox=\"0 0 52 34\" className=\"size-full overflow-visible drop-shadow-[0_1px_1.5px_oklch(0_0_0/0.3)]\">\n      {/* Tails, then loops, then the knot on top. */}\n      <path d=\"M24 19 L15 33 L19 32 L21 35 L26 21Z\" fill={RIBBON_DARK} />\n      <path d=\"M28 19 L37 33 L33 32 L31 35 L26 21Z\" fill={RIBBON_DARK} />\n      <path d=\"M26 17 C18 4, 4 2, 3 11 C2 20, 16 22, 26 17Z\" fill={RIBBON} />\n      <path d=\"M26 17 C34 4, 48 2, 49 11 C50 20, 36 22, 26 17Z\" fill={RIBBON} />\n      <path d=\"M26 17 C20 10, 10 8, 8 12\" stroke={RIBBON_DARK} strokeWidth=\"1.2\" fill=\"none\" strokeLinecap=\"round\" />\n      <path d=\"M26 17 C32 10, 42 8, 44 12\" stroke={RIBBON_DARK} strokeWidth=\"1.2\" fill=\"none\" strokeLinecap=\"round\" />\n      <rect x=\"21.5\" y=\"12.5\" width=\"9\" height=\"9\" rx=\"3\" fill={RIBBON_DARK} />\n    </svg>\n  );\n}\n\nfunction RollingNumber({ text, value }: { text: string; value: number }) {\n  const [previous, setPrevious] = useState(value);\n  const [direction, setDirection] = useState(1);\n  if (value !== previous) {\n    setPrevious(value);\n    setDirection(value > previous ? 1 : -1);\n  }\n  const chars = [...text];\n  return (\n    <span className=\"relative inline-flex tabular-nums\">\n      <span className=\"sr-only\">{text}</span>\n      <span aria-hidden className=\"inline-flex\">\n        {chars.map((char, i) => {\n          // Keyed by place from the right, so $9.50 to $10.00 keeps the\n          // cents columns and only adds a tens column.\n          const place = chars.length - i;\n          return /\\d/.test(char) ? (\n            <Digit key={`d${place}`} char={char} direction={direction} />\n          ) : (\n            <span key={`s${place}${char}`}>{char}</span>\n          );\n        })}\n      </span>\n    </span>\n  );\n}\n\nfunction Digit({ char, direction }: { char: string; direction: number }) {\n  const reduceMotion = useReducedMotion();\n  // Adding the fee rolls digits up; removing it rolls them back down.\n  const offset = (d: number) => (reduceMotion ? \"0%\" : `${d * 100}%`);\n  return (\n    <span className=\"inline-grid overflow-hidden\">\n      <AnimatePresence initial={false} custom={direction}>\n        <motion.span\n          key={char}\n          custom={direction}\n          variants={{\n            enter: (d: number) => ({ y: offset(d), opacity: 0 }),\n            center: { y: \"0%\", opacity: 1 },\n            exit: (d: number) => ({ y: offset(-d), opacity: 0 }),\n          }}\n          initial=\"enter\"\n          animate=\"center\"\n          exit=\"exit\"\n          transition={ROLL}\n          className=\"col-start-1 row-start-1\"\n        >\n          {char}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\n// A stand-in product photo: an amber glass candle jar with a paper label.\n// Raw colours because it is the product itself, not interface.\nfunction CandleJar() {\n  return (\n    <svg viewBox=\"0 0 72 72\" className=\"size-[72px]\" aria-hidden>\n      <defs>\n        <linearGradient id=\"gift-jar-glass\" x1=\"0\" x2=\"1\">\n          <stop offset=\"0\" stopColor=\"oklch(0.42 0.09 55)\" />\n          <stop offset=\"0.35\" stopColor=\"oklch(0.58 0.11 62)\" />\n          <stop offset=\"1\" stopColor=\"oklch(0.4 0.08 55)\" />\n        </linearGradient>\n        <linearGradient id=\"gift-jar-lid\" x1=\"0\" x2=\"1\">\n          <stop offset=\"0\" stopColor=\"oklch(0.3 0.01 60)\" />\n          <stop offset=\"0.4\" stopColor=\"oklch(0.5 0.01 60)\" />\n          <stop offset=\"1\" stopColor=\"oklch(0.28 0.01 60)\" />\n        </linearGradient>\n      </defs>\n      {/* Contact shadow on the table. */}\n      <ellipse cx=\"36\" cy=\"60\" rx=\"17\" ry=\"2.5\" fill=\"oklch(0 0 0 / 0.12)\" />\n      <rect x=\"20\" y=\"21\" width=\"32\" height=\"39\" rx=\"7\" fill=\"url(#gift-jar-glass)\" />\n      <rect x=\"21\" y=\"14\" width=\"30\" height=\"8\" rx=\"2\" fill=\"url(#gift-jar-lid)\" />\n      <rect x=\"23.5\" y=\"25\" width=\"3\" height=\"30\" rx=\"1.5\" fill=\"oklch(1 0 0 / 0.28)\" />\n      <rect x=\"26\" y=\"33\" width=\"20\" height=\"17\" rx=\"1.5\" fill=\"oklch(0.96 0.012 85)\" />\n      <rect x=\"30\" y=\"38\" width=\"12\" height=\"1.6\" rx=\"0.8\" fill=\"oklch(0.35 0.02 60)\" />\n      <rect x=\"32\" y=\"42.5\" width=\"8\" height=\"1.2\" rx=\"0.6\" fill=\"oklch(0.6 0.02 60)\" />\n    </svg>\n  );\n}\n\nexport default function GiftWrapDemo() {\n  return (\n    <GiftWrap\n      item={{\n        name: \"Hinoki Candle\",\n        variant: \"Hinoki and cedar, 220 g\",\n        price: 3800,\n        image: <CandleJar />,\n      }}\n      wrapFee={450}\n    />\n  );\n}\n"},{"path":"hooks/use-reduced-motion.ts","type":"registry:hook","target":"hooks/use-reduced-motion.ts","content":"\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\n\n// Motion's useReducedMotion reads the media query on the very first client\n// render, but the server can't know it, so every component that renders\n// differently under reduced motion broke hydration for those users. This\n// reports false during hydration, matching the server HTML, then the real\n// preference straight after, and follows it if it changes.\nconst query = \"(prefers-reduced-motion: reduce)\";\n\nfunction subscribe(onChange: () => void) {\n  const media = window.matchMedia(query);\n  media.addEventListener(\"change\", onChange);\n  return () => media.removeEventListener(\"change\", onChange);\n}\n\nexport function useReducedMotion() {\n  return useSyncExternalStore(\n    subscribe,\n    () => window.matchMedia(query).matches,\n    () => false,\n  );\n}\n"}],"cssVars":{"light":{"shadow-raised":"0 0 0 1px oklch(0 0 0 / 0.06), 0 1px 2px oklch(0 0 0 / 0.06), 0 6px 16px -6px oklch(0 0 0 / 0.12)"},"dark":{"shadow-raised":"0 0 0 1px oklch(1 0 0 / 0.08), 0 1px 2px oklch(0 0 0 / 0.4), 0 6px 16px -6px oklch(0 0 0 / 0.6)"},"theme":{"shadow-raised":"var(--shadow-raised)"}},"categories":["cards"],"docs":"From ui lab: https://lab.xevrion.dev/lab/gift-wrap"}