{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "waveform",
  "title": "Waveform",
  "description": "A clip waveform with seeking, hover, regions and markers.",
  "dependencies": [
    "solid-js@^1.9.15"
  ],
  "registryDependencies": [
    "@audiocn-solid/core",
    "@audiocn-solid/use-frame-source",
    "@audiocn-solid/use-visibility"
  ],
  "files": [
    {
      "path": "components/ui/waveform.tsx",
      "content": "import { Show, createContext, createSignal, useContext } from \"solid-js\";\n\nimport { useFrameSource } from \"@/hooks/use-frame-source\";\nimport { useVisibility } from \"@/hooks/use-visibility\";\nimport { resampleLevels } from \"@/lib/audio/bands\";\nimport { clamp } from \"@/lib/audio/decibels\";\nimport { subscribeFrame } from \"@/lib/audio/frame-loop\";\nimport { formatTime } from \"@/lib/audio/time\";\nimport type { FrameSource } from \"@/lib/audio/types\";\nimport { provideContext } from \"@/lib/solid/context\";\nimport { createCompatEffect } from \"@/lib/solid/effect\";\nimport type { CanvasDOMProps, DivDOMProps } from \"@/lib/solid/jsx-types\";\nimport { omitProps } from \"@/lib/solid/props\";\nimport { setRefValue } from \"@/lib/solid/ref\";\nimport type { RefTarget } from \"@/lib/solid/ref\";\nimport { mergeStyleVars } from \"@/lib/solid/style\";\nimport type { StyleValue } from \"@/lib/solid/style\";\nimport { cn } from \"@/lib/utils\";\n\ninterface WaveformContextValue {\n  readonly peaks: ArrayLike<number> | null;\n  readonly duration: number;\n  readonly variant: \"bars\" | \"line\" | \"mirror\";\n  readonly barWidth: number;\n  readonly barGap: number;\n  readonly barRadius: number;\n  readonly loading: boolean;\n  readonly progress: number;\n  readonly hover: number | null;\n  readonly interactive: boolean;\n  timeToPosition: (time: number) => number;\n}\n\nconst WaveformContext = createContext<WaveformContextValue>();\n\nconst useWaveform = (part: string) => {\n  const context = useContext(WaveformContext);\n\n  if (!context) throw new Error(`${part} must be used inside Waveform.`);\n\n  return context;\n};\n\ninterface WaveformCanvasProps extends Omit<CanvasDOMProps, \"ref\"> {\n  className?: string;\n  ref?: RefTarget<HTMLCanvasElement>;\n}\n\nexport const WaveformCanvas = (props: WaveformCanvasProps) => {\n  const settings = useWaveform(\"WaveformCanvas\");\n  let canvas: HTMLCanvasElement | undefined;\n  const visible = useVisibility(() => canvas ?? null);\n  createCompatEffect(\n    () => ({\n      barGap: settings.barGap,\n      barRadius: settings.barRadius,\n      barWidth: settings.barWidth,\n      loading: settings.loading,\n      peaks: settings.peaks,\n      variant: settings.variant,\n    }),\n    (options) => {\n      const element = canvas;\n      const context = element?.getContext(\"2d\");\n\n      if (!(element && context)) return;\n      const size = { width: 0, height: 0 };\n      let ratio = 1;\n      let levels = new Float32Array(0);\n      let lastProgress = -1;\n      let colors = { played: \"\", unplayed: \"\" };\n      let sinceColor = 30;\n\n      const resize = () => {\n        const rect = element.getBoundingClientRect();\n        size.width = rect.width;\n        size.height = rect.height;\n        ratio = window.devicePixelRatio || 1;\n        element.width = Math.max(1, Math.round(size.width * ratio));\n        element.height = Math.max(1, Math.round(size.height * ratio));\n\n        const count = Math.max(\n          1,\n          Math.floor(\n            (size.width + options.barGap) / (options.barWidth + options.barGap)\n          )\n        );\n\n        levels = new Float32Array(count);\n\n        if (options.peaks?.length)\n          resampleLevels(options.peaks, 0, options.peaks.length, levels);\n        lastProgress = -1;\n      };\n\n      const drawPass = (color: string, width: number) => {\n        context.save();\n        context.beginPath();\n        context.rect(0, 0, width, size.height);\n        context.clip();\n        context.fillStyle = color;\n        context.strokeStyle = color;\n        const pitch = options.barWidth + options.barGap;\n        const middle = size.height / 2;\n\n        if (options.variant === \"line\") {\n          context.beginPath();\n\n          for (let index = 0; index < levels.length; index++) {\n            const x = index * pitch + options.barWidth / 2;\n            const y = middle - (levels[index] ?? 0) * (middle - 1);\n\n            if (index === 0) context.moveTo(x, y);\n            else context.lineTo(x, y);\n          }\n\n          for (let index = levels.length - 1; index >= 0; index--) {\n            context.lineTo(\n              index * pitch + options.barWidth / 2,\n              middle + (levels[index] ?? 0) * (middle - 1)\n            );\n          }\n\n          context.closePath();\n          context.fill();\n        } else {\n          for (let index = 0; index < levels.length; index++) {\n            const height = Math.max(2, (levels[index] ?? 0) * size.height);\n\n            const y =\n              options.variant === \"mirror\"\n                ? middle - height / 2\n                : size.height - height;\n\n            context.beginPath();\n            context.roundRect(\n              index * pitch,\n              y,\n              options.barWidth,\n              height,\n              Math.min(options.barRadius, options.barWidth / 2, height / 2)\n            );\n            context.fill();\n          }\n        }\n\n        context.restore();\n      };\n\n      resize();\n      const observer = new ResizeObserver(resize);\n      observer.observe(element);\n\n      const unsubscribe = subscribeFrame(() => {\n        if (++sinceColor >= 30) {\n          sinceColor = 0;\n          const style = getComputedStyle(element);\n\n          const next = {\n            played:\n              style.getPropertyValue(\"--waveform-progress\").trim() ||\n              style.color,\n            unplayed:\n              style.getPropertyValue(\"--waveform\").trim() || style.color,\n          };\n\n          if (\n            next.played !== colors.played ||\n            next.unplayed !== colors.unplayed\n          ) {\n            colors = next;\n            lastProgress = -1;\n          }\n        }\n\n        const progress = settings.progress;\n\n        if (progress === lastProgress || !size.width || !visible.current)\n          return;\n        lastProgress = progress;\n        context.setTransform(ratio, 0, 0, ratio, 0, 0);\n        context.clearRect(0, 0, size.width, size.height);\n\n        if (options.loading || !options.peaks) return;\n        drawPass(colors.unplayed, size.width);\n        drawPass(colors.played, progress * size.width);\n      });\n\n      return () => {\n        unsubscribe();\n        observer.disconnect();\n      };\n    }\n  );\n  const rest = omitProps(props, [\"class\", \"className\", \"ref\"]);\n\n  return (\n    <>\n      <Show when={settings.loading}>\n        <div\n          aria-hidden=\"true\"\n          class=\"bg-muted absolute inset-0 animate-pulse rounded-lg motion-reduce:animate-none\"\n          data-slot=\"waveform-skeleton\"\n        />\n      </Show>\n      <canvas\n        aria-hidden=\"true\"\n        class={cn(\"absolute inset-0 size-full\", props.class, props.className)}\n        data-slot=\"waveform-canvas\"\n        {...rest}\n        ref={(node: HTMLCanvasElement) => {\n          canvas = node;\n          setRefValue(props.ref, node);\n        }}\n      />\n    </>\n  );\n};\n\ninterface WaveformPartProps extends Omit<DivDOMProps, \"style\"> {\n  className?: string;\n  style?: StyleValue;\n}\n\nexport const WaveformCursor = (props: WaveformPartProps) => {\n  const rest = omitProps(props, [\"class\", \"className\"]);\n\n  return (\n    <div\n      aria-hidden=\"true\"\n      class={cn(\n        \"pointer-events-none absolute inset-y-0 left-[calc(var(--waveform-position)*100%)] w-0.5 -translate-x-1/2 rounded-full bg-(--waveform-cursor) group-data-loading/waveform:hidden\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"waveform-cursor\"\n      {...rest}\n    />\n  );\n};\n\nexport interface WaveformHoverProps extends WaveformPartProps {\n  format?: (time: number) => string;\n}\n\nexport const WaveformHover = (props: WaveformHoverProps) => {\n  const settings = useWaveform(\"WaveformHover\");\n  const rest = omitProps(props, [\"class\", \"className\", \"format\", \"style\"]);\n\n  return (\n    <Show when={settings.interactive && settings.hover !== null}>\n      <div\n        aria-hidden=\"true\"\n        class={cn(\n          \"bg-foreground/40 pointer-events-none absolute inset-y-0 left-(--waveform-hover) w-px\",\n          props.class,\n          props.className\n        )}\n        data-slot=\"waveform-hover\"\n        style={mergeStyleVars(props.style, {\n          \"--waveform-hover\": `${settings.timeToPosition(settings.hover ?? 0) * 100}%`,\n        })}\n        {...rest}\n      >\n        <span class=\"bg-foreground text-background absolute -top-6 left-1/2 -translate-x-1/2 rounded-md px-1.5 py-0.5 font-mono text-[0.625rem] whitespace-nowrap tabular-nums\">\n          {(props.format ?? formatTime)(settings.hover ?? 0)}\n        </span>\n      </div>\n    </Show>\n  );\n};\n\nexport interface WaveformRegionValue {\n  start: number;\n  end: number;\n}\n\nexport interface WaveformRegionProps extends Omit<\n  WaveformPartProps,\n  \"onChange\" | \"defaultValue\" | \"ref\" | \"draggable\"\n> {\n  ref?: RefTarget<HTMLDivElement>;\n  start: number;\n  end: number;\n  onValueChange?: (value: WaveformRegionValue) => void;\n  resizable?: boolean;\n  draggable?: boolean;\n  minLength?: number;\n}\n\ninterface RegionDrag {\n  kind: \"start\" | \"end\" | \"move\";\n  x: number;\n  start: number;\n  end: number;\n}\n\nconst HANDLE_CLASS =\n  \"absolute inset-y-0 w-2 cursor-ew-resize rounded-sm bg-primary/60 after:absolute after:inset-y-0 after:-inset-x-1.5 pointer-coarse:after:-inset-x-3 outline-none focus-visible:bg-primary focus-visible:ring-3 focus-visible:ring-ring/30\";\n\nexport const WaveformRegion = (props: WaveformRegionProps) => {\n  const settings = useWaveform(\"WaveformRegion\");\n  let region: HTMLDivElement | undefined;\n  let drag: RegionDrag | null = null;\n\n  const begin = (kind: RegionDrag[\"kind\"], event: PointerEvent) => {\n    event.stopPropagation();\n    const target = event.currentTarget;\n\n    if (!(target instanceof HTMLElement) || event.button !== 0) return;\n    target.setPointerCapture(event.pointerId);\n    drag = { kind, x: event.clientX, start: props.start, end: props.end };\n  };\n\n  const move = (event: PointerEvent) => {\n    if (!drag) return;\n    event.stopPropagation();\n    const width = region?.parentElement?.getBoundingClientRect().width ?? 1;\n    const delta = ((event.clientX - drag.x) * settings.duration) / width;\n    const min = props.minLength ?? 0.1;\n\n    if (drag.kind === \"move\") {\n      const length = drag.end - drag.start;\n      const start = clamp(drag.start + delta, 0, settings.duration - length);\n      props.onValueChange?.({ start, end: start + length });\n    } else if (drag.kind === \"start\")\n      props.onValueChange?.({\n        start: clamp(drag.start + delta, 0, drag.end - min),\n        end: drag.end,\n      });\n    else\n      props.onValueChange?.({\n        start: drag.start,\n        end: clamp(drag.end + delta, drag.start + min, settings.duration),\n      });\n  };\n\n  const finish = (event: PointerEvent) => {\n    event.stopPropagation();\n    drag = null;\n  };\n\n  const nudge = (edge: \"start\" | \"end\", event: KeyboardEvent) => {\n    // Region keys must not bubble to the clip's seek handler.\n    event.stopPropagation();\n    const amount = event.shiftKey ? 1 : 0.1;\n\n    const delta =\n      event.key === \"ArrowRight\" || event.key === \"ArrowUp\"\n        ? amount\n        : event.key === \"ArrowLeft\" || event.key === \"ArrowDown\"\n          ? -amount\n          : 0;\n\n    if (!delta) return;\n    event.preventDefault();\n    const min = props.minLength ?? 0.1;\n    props.onValueChange?.(\n      edge === \"start\"\n        ? {\n            start: clamp(props.start + delta, 0, props.end - min),\n            end: props.end,\n          }\n        : {\n            start: props.start,\n            end: clamp(props.end + delta, props.start + min, settings.duration),\n          }\n    );\n  };\n\n  const rest = omitProps(props, [\n    \"class\",\n    \"className\",\n    \"style\",\n    \"children\",\n    \"start\",\n    \"end\",\n    \"onValueChange\",\n    \"resizable\",\n    \"draggable\",\n    \"minLength\",\n    \"ref\",\n  ]);\n\n  return (\n    <div\n      class={cn(\n        \"bg-primary/15 ring-primary/40 absolute inset-y-0 left-(--region-start) w-(--region-size) rounded-sm ring-1\",\n        (props.draggable ?? true) && \"cursor-grab active:cursor-grabbing\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"waveform-region\"\n      onPointerDown={(event: PointerEvent) => {\n        if (props.draggable ?? true) begin(\"move\", event);\n        else event.stopPropagation();\n      }}\n      onPointerMove={move}\n      onPointerUp={finish}\n      onPointerCancel={finish}\n      onLostPointerCapture={finish}\n      style={mergeStyleVars(props.style, {\n        \"--region-start\": `${settings.timeToPosition(props.start) * 100}%`,\n        \"--region-size\": `${(settings.timeToPosition(props.end) - settings.timeToPosition(props.start)) * 100}%`,\n      })}\n      {...rest}\n      ref={(node: HTMLDivElement) => {\n        region = node;\n        setRefValue(props.ref, node);\n      }}\n    >\n      {props.children}\n      <Show when={props.resizable ?? true}>\n        <span\n          aria-label=\"Region start\"\n          aria-valuemin={0}\n          aria-valuemax={props.end}\n          aria-valuenow={props.start}\n          aria-valuetext={formatTime(props.start)}\n          class={cn(HANDLE_CLASS, \"-left-1\")}\n          data-slot=\"waveform-region-start\"\n          role=\"slider\"\n          tabindex={0}\n          onPointerDown={(event: PointerEvent) => begin(\"start\", event)}\n          onKeyDown={(event: KeyboardEvent) => nudge(\"start\", event)}\n        />\n        <span\n          aria-label=\"Region end\"\n          aria-valuemin={props.start}\n          aria-valuemax={settings.duration}\n          aria-valuenow={props.end}\n          aria-valuetext={formatTime(props.end)}\n          class={cn(HANDLE_CLASS, \"-right-1\")}\n          data-slot=\"waveform-region-end\"\n          role=\"slider\"\n          tabindex={0}\n          onPointerDown={(event: PointerEvent) => begin(\"end\", event)}\n          onKeyDown={(event: KeyboardEvent) => nudge(\"end\", event)}\n        />\n      </Show>\n    </div>\n  );\n};\n\nexport interface WaveformMarkerProps extends WaveformPartProps {\n  time: number;\n}\n\nexport const WaveformMarker = (props: WaveformMarkerProps) => {\n  const settings = useWaveform(\"WaveformMarker\");\n\n  const rest = omitProps(props, [\n    \"class\",\n    \"className\",\n    \"style\",\n    \"children\",\n    \"time\",\n  ]);\n\n  return (\n    <div\n      class={cn(\n        \"bg-meter-warn pointer-events-none absolute inset-y-0 left-(--marker-position) w-px\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"waveform-marker\"\n      style={mergeStyleVars(props.style, {\n        \"--marker-position\": `${settings.timeToPosition(props.time) * 100}%`,\n      })}\n      {...rest}\n    >\n      <Show when={props.children}>\n        <span class=\"bg-meter-warn/20 text-foreground absolute top-0 left-1 rounded-sm px-1 text-[0.625rem] font-medium whitespace-nowrap\">\n          {props.children}\n        </span>\n      </Show>\n    </div>\n  );\n};\n\nexport interface WaveformProps extends Omit<\n  WaveformPartProps,\n  \"onSeeked\" | \"defaultValue\" | \"ref\"\n> {\n  peaks: ArrayLike<number> | null;\n  duration: number;\n  currentTime?: number;\n  defaultCurrentTime?: number;\n  time?: FrameSource<number> | null;\n  onSeek?: (time: number) => void;\n  onSeekCommitted?: (time: number) => void;\n  step?: number;\n  largeStep?: number;\n  variant?: \"bars\" | \"line\" | \"mirror\";\n  barWidth?: number;\n  barGap?: number;\n  barRadius?: number;\n  interactive?: boolean;\n  loading?: boolean;\n  disabled?: boolean;\n  ref?: RefTarget<HTMLDivElement>;\n}\n\nexport const Waveform = (props: WaveformProps) => {\n  const [internalTime, setInternalTime] = createSignal(\n    props.defaultCurrentTime ?? 0\n  );\n\n  const [hover, setHover] = createSignal<number | null>(null);\n  const [dragging, setDragging] = createSignal(false);\n  let root: HTMLDivElement | undefined;\n  let progress = 0;\n  let latestTime = props.currentTime ?? props.defaultCurrentTime ?? 0;\n  let playbackTime = latestTime;\n  let pointerId: number | null = null;\n  const shownTime = () => props.currentTime ?? internalTime();\n\n  const position = (time: number) =>\n    props.duration > 0 ? clamp(time / props.duration, 0, 1) : 0;\n\n  const write = (time: number) => {\n    latestTime = time;\n    progress = position(time);\n    root?.style.setProperty(\"--waveform-position\", progress.toFixed(5));\n  };\n\n  const receivePlayback = (time: number) => {\n    playbackTime = time;\n\n    if (pointerId === null) write(time);\n  };\n\n  createCompatEffect(\n    () => ({ time: shownTime(), duration: props.duration }),\n    ({ time }) => receivePlayback(time)\n  );\n  useFrameSource(() => props.time, receivePlayback);\n\n  const active = () =>\n    (props.interactive ?? true) &&\n    !props.loading &&\n    !props.disabled &&\n    props.duration > 0;\n\n  const seek = (time: number, commit: boolean) => {\n    const next = clamp(time, 0, props.duration);\n    write(next);\n\n    if (props.currentTime === undefined && (commit || pointerId === null))\n      setInternalTime(next);\n    props.onSeek?.(next);\n\n    if (commit) props.onSeekCommitted?.(next);\n  };\n\n  const cancelDrag = (event?: PointerEvent) => {\n    if (pointerId === null || (event && event.pointerId !== pointerId)) return;\n    const captured = pointerId;\n    pointerId = null;\n    setDragging(false);\n    write(playbackTime);\n\n    if (root?.hasPointerCapture(captured)) root.releasePointerCapture(captured);\n  };\n\n  createCompatEffect(active, (enabled) => {\n    if (!enabled) cancelDrag();\n  });\n\n  const timeAtPointer = (event: PointerEvent) => {\n    const rect = root!.getBoundingClientRect();\n\n    return (\n      clamp((event.clientX - rect.left) / rect.width, 0, 1) * props.duration\n    );\n  };\n\n  const keyDown = (event: KeyboardEvent) => {\n    if (!active() || event.target !== event.currentTarget) return;\n    const amount = event.shiftKey ? (props.largeStep ?? 15) : (props.step ?? 5);\n\n    const targets = {\n      ArrowDown: latestTime - amount,\n      ArrowLeft: latestTime - amount,\n      ArrowRight: latestTime + amount,\n      ArrowUp: latestTime + amount,\n      Home: 0,\n      End: props.duration,\n    };\n\n    if (!Object.hasOwn(targets, event.key)) return;\n    event.preventDefault();\n    // SAFETY: The membership check above proves the key names a seek target.\n    seek(targets[event.key as keyof typeof targets], true);\n  };\n\n  const settings: WaveformContextValue = {\n    get peaks() {\n      return props.peaks;\n    },\n    get duration() {\n      return props.duration;\n    },\n    get variant() {\n      return props.variant ?? \"bars\";\n    },\n    get barWidth() {\n      return props.barWidth ?? 2;\n    },\n    get barGap() {\n      return props.barGap ?? 1;\n    },\n    get barRadius() {\n      return props.barRadius ?? 1;\n    },\n    get loading() {\n      return props.loading ?? false;\n    },\n    get progress() {\n      return progress;\n    },\n    get hover() {\n      return hover();\n    },\n    get interactive() {\n      return active();\n    },\n    timeToPosition: position,\n  };\n\n  const rest = omitProps(props, [\n    \"class\",\n    \"className\",\n    \"children\",\n    \"ref\",\n    \"peaks\",\n    \"duration\",\n    \"currentTime\",\n    \"defaultCurrentTime\",\n    \"time\",\n    \"onSeek\",\n    \"onSeekCommitted\",\n    \"step\",\n    \"largeStep\",\n    \"variant\",\n    \"barWidth\",\n    \"barGap\",\n    \"barRadius\",\n    \"interactive\",\n    \"loading\",\n    \"disabled\",\n  ]);\n\n  return provideContext(WaveformContext, settings, () => (\n    <div\n      class={cn(\n        \"group/waveform focus-visible:ring-ring/30 relative h-20 w-full touch-none rounded-lg outline-none select-none [--waveform-cursor:var(--foreground)] [--waveform-position:0] [--waveform-progress:var(--primary)] [--waveform:var(--muted-foreground)] focus-visible:ring-3 data-disabled:opacity-50\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"waveform\"\n      data-variant={settings.variant}\n      data-disabled={props.disabled ? \"\" : undefined}\n      data-loading={props.loading ? \"\" : undefined}\n      data-dragging={dragging() ? \"\" : undefined}\n      role={active() ? \"slider\" : undefined}\n      tabindex={active() ? 0 : undefined}\n      aria-valuemin={active() ? 0 : undefined}\n      aria-valuemax={active() ? Math.round(props.duration) : undefined}\n      aria-valuenow={active() ? Math.round(shownTime()) : undefined}\n      aria-valuetext={\n        active()\n          ? `${formatTime(shownTime())} of ${formatTime(props.duration)}`\n          : undefined\n      }\n      onKeyDown={keyDown}\n      onPointerDown={(event: PointerEvent) => {\n        if (!active() || event.button !== 0 || pointerId !== null) return;\n        pointerId = event.pointerId;\n        root!.setPointerCapture(event.pointerId);\n        setDragging(true);\n        seek(timeAtPointer(event), false);\n      }}\n      onPointerMove={(event: PointerEvent) => {\n        if (!active()) return;\n        const time = timeAtPointer(event);\n        setHover(time);\n\n        if (pointerId === event.pointerId) seek(time, false);\n      }}\n      onPointerLeave={() => setHover(null)}\n      onPointerUp={(event: PointerEvent) => {\n        if (pointerId !== event.pointerId) return;\n        const next = latestTime;\n        pointerId = null;\n        setDragging(false);\n\n        if (active()) seek(next, true);\n        else write(playbackTime);\n      }}\n      onPointerCancel={cancelDrag}\n      onLostPointerCapture={cancelDrag}\n      {...rest}\n      ref={(node: HTMLDivElement) => {\n        root = node;\n        setRefValue(props.ref, node);\n      }}\n    >\n      {props.children ?? (\n        <>\n          <WaveformCanvas />\n          <WaveformCursor />\n        </>\n      )}\n    </div>\n  ));\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "lib/audiocn-license.txt",
      "content": "Audio component source adapted from https://github.com/audiocn/ui.\nMIT License — Copyright (c) 2026 OrcDev\n\nRegular SVG icon geometry adapted from @phosphor-icons/react 2.1.10.\nMIT License — Copyright (c) 2020 Phosphor Icons\n\nThe following permission and warranty terms apply to both notices.\nRetain this file with copies or substantial portions of the source.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
      "type": "registry:file",
      "target": "@lib/audiocn-license.txt"
    }
  ],
  "type": "registry:ui"
}
