{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "surface",
  "type": "registry:ui",
  "title": "Surface",
  "description": "A composable in-view reveal wrapper with fade/slide/scale/blur presets and staggered children. Default melt preset rises and sharpens with a soft white under-glow, like surfacing out of the dark.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://crucibleui.com/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/default/scroll/surface/surface.tsx",
      "type": "registry:ui",
      "target": "components/crucible/surface.tsx",
      "content": "\"use client\";\n\nimport { motion, type HTMLMotionProps, type TargetAndTransition, type Variants } from \"motion/react\";\nimport * as React from \"react\";\n\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface SurfaceProps\n  extends Omit<HTMLMotionProps<\"div\">, \"children\" | \"initial\" | \"whileInView\" | \"viewport\" | \"transition\"> {\n  /** Content to reveal. Each top-level child animates as its own staggered item. */\n  children: React.ReactNode;\n  /**\n   * Named entrance treatment. `melt` is the Crucible signature: rise + blur +\n   * a soft under-glow, like the element is surfacing out of the dark.\n   * @default \"melt\"\n   */\n  preset?: \"melt\" | \"fade\" | \"slide\" | \"scale\" | \"blur\";\n  /** Travel direction for `melt`/`slide`. @default \"up\" */\n  direction?: \"up\" | \"down\" | \"left\" | \"right\";\n  /** Travel distance in px for `melt`/`slide`. @default 12 */\n  distance?: number;\n  /** Starting blur radius in px for `melt`/`blur`. @default 8 */\n  blur?: number;\n  /** Starting scale for `scale`. @default 0.94 */\n  scaleFrom?: number;\n  /** Seconds each item takes to settle. @default 0.6 */\n  duration?: number;\n  /** Seconds between each top-level child's reveal. @default 0.08 */\n  stagger?: number;\n  /** Seconds before the first item starts. @default 0 */\n  delay?: number;\n  /** Re-trigger every time it scrolls into view instead of once. @default false */\n  repeat?: boolean;\n  /** Fraction of the element that must be visible to trigger. @default 0.4 */\n  amount?: number;\n  /** `viewport.margin`-style rootMargin, e.g. to trigger before entry. @default \"0px 0px -10% 0px\" */\n  margin?: NonNullable<HTMLMotionProps<\"div\">[\"viewport\"]>[\"margin\"];\n  /**\n   * Scroll container to observe instead of the browser viewport. Pass a ref to\n   * an `overflow`-scrolling ancestor so the reveal triggers as content scrolls\n   * within THAT container (e.g. inside a boxed panel). @default the viewport\n   */\n  root?: React.RefObject<Element | null>;\n  /** Show the under-glow (only rendered for `preset=\"melt\"`). @default true */\n  ember?: boolean;\n  /** Glow color for the `melt` under-glow. @default \"#ffffff\" (soft white) */\n  emberColor?: string;\n}\n\nfunction buildItemVariants(opts: {\n  preset: SurfaceProps[\"preset\"];\n  direction: NonNullable<SurfaceProps[\"direction\"]>;\n  distance: number;\n  blur: number;\n  scaleFrom: number;\n  duration: number;\n  reducedMotion: boolean;\n}): Variants {\n  const { preset, direction, distance, blur, scaleFrom, duration, reducedMotion } = opts;\n\n  if (reducedMotion) {\n    // Reset the full superset of animatable props so the reveal lands on a clean\n    // static frame even if an earlier (pre-hydration) render committed a melt/slide\n    // hidden state carrying a blur or offset.\n    return {\n      hidden: { opacity: 0 },\n      show: { opacity: 1, x: 0, y: 0, filter: \"blur(0px)\", scale: 1, transition: { duration: 0.3 } },\n    };\n  }\n\n  const axis = direction === \"left\" || direction === \"right\" ? \"x\" : \"y\";\n  const offset = axis === \"y\" ? (direction === \"up\" ? distance : -distance) : direction === \"left\" ? distance : -distance;\n\n  const usesOffset = preset === \"melt\" || preset === \"slide\";\n  const usesBlur = preset === \"melt\" || preset === \"blur\";\n  const usesScale = preset === \"scale\";\n\n  const hidden: TargetAndTransition = {\n    opacity: 0,\n    ...(usesOffset ? { [axis]: offset } : {}),\n    ...(usesBlur ? { filter: `blur(${blur}px)` } : {}),\n    ...(usesScale ? { scale: scaleFrom } : {}),\n  };\n\n  const show: TargetAndTransition = {\n    opacity: 1,\n    ...(usesOffset ? { [axis]: 0 } : {}),\n    ...(usesBlur ? { filter: \"blur(0px)\" } : {}),\n    ...(usesScale ? { scale: 1 } : {}),\n    transition: { duration, ease: [0.25, 0.4, 0.25, 1] },\n  };\n\n  return { hidden, show };\n}\n\n/**\n * Surface — a composable in-view reveal wrapper. Wrap any content and it rises\n * into view as it crosses the viewport threshold; wrap multiple top-level\n * children and they stagger in sequence, each animating via shared variants.\n *\n * Default `melt` preset reads as surfacing out of the dark: children rise 12px\n * while a soft white under-glow brightens then fades beneath them, with a\n * starting blur that sharpens as they settle. Falls back to a plain opacity\n * fade when the user prefers reduced motion.\n */\nexport function Surface({\n  children,\n  preset = \"melt\",\n  direction = \"up\",\n  distance = 12,\n  blur = 8,\n  scaleFrom = 0.94,\n  duration = 0.6,\n  stagger = 0.08,\n  delay = 0,\n  repeat = false,\n  amount = 0.4,\n  margin = \"0px 0px -10% 0px\",\n  root,\n  ember = true,\n  emberColor = \"#ffffff\",\n  className,\n  ...props\n}: SurfaceProps) {\n  const reducedMotion = useReducedMotion();\n  const items = React.Children.toArray(children);\n\n  const itemVariants = React.useMemo(\n    () => buildItemVariants({ preset, direction, distance, blur, scaleFrom, duration, reducedMotion }),\n    [preset, direction, distance, blur, scaleFrom, duration, reducedMotion]\n  );\n\n  const glowVariants: Variants = {\n    hidden: { opacity: 0 },\n    show: {\n      opacity: [0, 0.55, 0],\n      transition: { duration: duration * 1.4, times: [0, 0.45, 1], ease: \"easeOut\" },\n    },\n  };\n\n  const showEmber = ember && preset === \"melt\" && !reducedMotion;\n\n  return (\n    <motion.div\n      data-crucible=\"surface\"\n      className={cn(\"relative isolate\", className)}\n      initial=\"hidden\"\n      whileInView=\"show\"\n      viewport={{ once: !repeat, amount, margin, root }}\n      transition={{ staggerChildren: reducedMotion ? 0 : stagger, delayChildren: delay }}\n      {...props}\n    >\n      {showEmber && (\n        <motion.span\n          aria-hidden\n          variants={glowVariants}\n          className=\"pointer-events-none absolute inset-x-0 bottom-0 -z-10 h-1/2 blur-2xl\"\n          style={{ background: `radial-gradient(60% 100% at 50% 100%, ${emberColor}55, transparent 70%)` }}\n        />\n      )}\n      {items.map((child, i) => (\n        <motion.div\n          key={i}\n          variants={itemVariants}\n          className=\"will-change-[transform,filter,opacity]\"\n        >\n          {child}\n        </motion.div>\n      ))}\n    </motion.div>\n  );\n}\n"
    }
  ]
}